Harness : quand l'agent améliore lui-même le harness qui l'entoure
Quand un agent déçoit, le premier réflexe est de changer de modèle. Pourtant, un même modèle peut passer de décevant à convaincant selon son prompt système, les outils qu’on lui décrit ou sa façon de relire son travail avant de rendre un livrable. Cet environnement a un nom : le harness. Google Research vient de publier en open source Harness², un framework qui le fait évoluer automatiquement, sans toucher aux poids du modèle. Le papier et le site du projet détaillent la méthode, les chiffres et les limites.
> TL;DR
>
> - Harness² optimise le harness d’un agent (prompt, mémoire, outils, sous-agents…) au lieu du modèle.
> - Deux boucles : une par tâche (Propose, Probe, Compose), une qui apprend de tâche en tâche.
> - Selon Google, une seule étape suffit à améliorer GLM-5 et Gemini 3.5 Flash sur trois benchmarks. Le coût en calcul et la vérification sans réponse de référence restent les points à surveiller.
Nous verrons ce qu’est un harness, comment fonctionnent les deux niveaux de récursion, puis nous lancerons la démo locale avant de regarder les limites.
Le harness : tout ce qui entoure le modèle
Un modèle seul ne fait que produire du texte. Ce qui en fait un agent, c’est le système autour : prompts, outils, mémoire, orchestration. Harness² en expose huit composants modifiables :
- Construction du contexte : prompt système, garde-fous, mémoire, skills
- Interface d’outils : consignes d’utilisation des outils, scripts
- Orchestration : sous-agents
- Middleware : plugins
!Les huit composants du harness regroupés par rôle à l’exécution : contexte, outils, délégation et middleware
L’optimisation de ce système s’appelle le _harnessing_. Les équipes qui maintiennent un AGENTS.md ou des skills à la main font déjà cette boucle : observer un échec, réécrire une consigne, relancer. Harness² la confie à des agents.
Premier niveau : Propose, Probe, Compose
Pour la tâche en cours, le framework enchaîne trois étapes :
1. Propose : un éditeur analyse une première exécution et propose des modifications du harness. Il en existe deux familles : des règles générales de comportement (par exemple relire un fichier après l’avoir écrit) et des conseils propres au domaine de la tâche.
2. Probe : chaque variante est exécutée sur la même tâche.
3. Compose : un composeur compare les trajectoires obtenues (appels d’outils, observations, fichiers produits) et assemble un harness final à partir des changements dont l’effet est observable.
Aucune réponse de référence n’est utilisée : le composeur s’appuie uniquement sur la comparaison des comportements entre exécutions.
!Pipeline de Harness² : l’exécuteur lance le harness initial, l’éditeur propose des modifications, les sondes les testent et le composeur produit le harness final
Second niveau : apprendre à améliorer
Sans mémoire, chaque tâche repartirait de zéro. Après chaque tâche, Harness² conserve donc deux choses dans un _improvement harness_ persistant : le harness qui a été adopté, et les leçons que les traces d’exécution permettent d’appuyer (par exemple, relire le fichier final avant de le rendre). Ces leçons guident les propositions faites sur les tâches suivantes.
En revanche, une nouvelle tâche ne reprend pas le harness spécialisé de la précédente : elle repart du harness de base. Un réglage pensé pour analyser un contrat pourrait gêner sur une tâche de tableur, donc le framework garde ce qu’il a appris, pas la configuration elle-même.
Parallèle ou séquentiel
Le budget de récursion se règle avec --k, de deux façons :
| Mode | Comportement |
| --- | --- |
| --mode parallel | Teste K candidats généraux et K candidats de domaine, puis produit K compositions |
| --mode sequential | Enchaîne K étapes Propose, Probe, Compose, en reportant le harness affiné et les preuves |
Le mode parallèle explore plus large, le séquentiel affine plus en profondeur. Dans les deux modes, dès que K dépasse 1, un sélecteur compare à l’aveugle les livrables obtenus et n’en garde qu’un, sans savoir de quel harness il vient.
!Les deux modes de récursion : parallèle avec k candidats testés simultanément, séquentiel avec k étapes successives
Essayer en local
La démo ne demande aucun identifiant de modèle : elle lance les deux modes avec des éditeurs déterministes et de vraies exécutions Python. Le dépôt indique Linux et Python 3.12 comme prérequis, mais la démo a aussi tourné sur macOS lors de nos essais.
# Environnement Python 3.12 avec uv
uv venv --python 3.12 .venv
source .venv/bin/activate
# Installation du projet depuis la racine du dépôt cloné
uv pip install -e '.[render]'
# Démo locale, budget de récursion k=2
harness2-demo --output runs/demo --k 2
Pour les benchmarks, Harness² ne remplace pas votre agent : il le pilote. Cet agent s’appelle l’exécuteur. C’est OpenCode par défaut, ou Codex (l’agent de code d’OpenAI) avec l’option --substrate cx. Il faut aussi accéder à des modèles via Vertex AI, et installer Node.js 20+ et gcloud. L’exemple du README lance une tâche du benchmark LAB, puis l’évalue et produit le rapport :
harness2-setup lab
harness2-check --bench lab
harness2 --bench lab --k 1 --mode parallel --only-domains antitrust-competition --max-tasks 1
harness2-judge --bench lab --k 1 --mode parallel
harness2-report --bench lab --k 1 --mode parallel
Ces runs consomment des appels API : le README recommande de commencer par une petite sélection. Attention, --max-tasks plafonne le nombre de tâches par domaine, pas sur l’ensemble du run.
Résultats annoncés et limites
Google a évalué Harness² sur trois benchmarks : JobBench (travail professionnel), LAB (tâches juridiques) et WorkBuddy-Bench (bureautique, web et code). Selon ses auteurs, une seule étape améliore GLM-5 et Gemini 3.5 Flash par rapport à leur harness de base : jusqu’à +11,5 points sur WorkBuddy-Bench, +4,5 sur JobBench et +11,8 points de pourcentage sur le taux de réussite partielle de LAB. Un budget de récursion plus élevé (un --k plus grand) apporte des gains supplémentaires.
Deux détails pèsent dans la lecture de ces résultats, détaillés dans le papier. L’éditeur et le sélecteur sont Claude Opus 4.8, et le juge de JobBench aussi : un biais est possible. Les échantillons sont petits : 34 tâches de test pour JobBench et 100 pour LAB, avec des scores moyennés sur 3 runs.
Le calcul d’abord : chaque proposition, chaque sonde et chaque composition est une exécution de plus de l’agent, donc des tokens et du temps. Plus --k est élevé, plus la facture grimpe.
Le choix du meilleur candidat ensuite. Aucune bonne réponse n’est connue pendant l’exécution : le composeur s’appuie sur le comportement observé et, dès que --k dépasse 1, le sélecteur final choisit sans connaître la bonne réponse. Il peut se tromper. Les auteurs relèvent eux-mêmes des régressions : sur une tâche de code de WorkBuddy-Bench, le sélecteur a retenu une sortie notée 55 alors qu’une autre atteignait 95.
La sécurité enfin. Garde-fous, scripts et plugins font partie du harness, donc l’éditeur automatique peut les modifier. Côté exécution, l’agent a un accès réseau sans restriction, et l’exécuteur Codex tourne sans sandbox. Le README demande de restreindre le réseau avant toute évaluation, car les critères de notation des benchmarks peuvent se trouver en ligne.
Reste le statut du projet : le README précise que ce n’est pas un produit officiellement supporté par Google. À traiter comme un projet à expérimenter avant de l’envisager en production.
Et ensuite ?
Harness² propose de traiter le harness comme un ensemble de fichiers que l’on peut lire et mesurer, plutôt que comme un réglage fait une fois à la main. Son point faible est la vérification : sans bonne réponse connue, rien ne garantit qu’un harness « amélioré » le soit vraiment. Pour tester sans risque, une équipe peut garder un harness de base figé, laisser les révisions se faire dans un environnement isolé, puis comparer la qualité obtenue au calcul dépensé.
🎓 Aller plus loin avec les agents
Vous voulez construire votre propre agent IA ? En une journée, la formation Développer son propre agent de TechTown couvre :
- la différence entre un agent et un chatbot ;
- l’architecture d’un agent : sessions, mémoire, grounding et MCP ;
- les tests, les guardrails et le déploiement ;
- trois ateliers pour créer un agent avec ADK, lui ajouter des tools et le brancher à un serveur MCP.
Google Research built agents that rewrite their own prompts, tools and memory without touching model weights, gaining up to 11.8 points on pro benchmarks. 🔗 google-research/harness2
Credits: google-research/harness2
#harness2 #python #aiagents
2 likes
[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](/2026/10/16/adk-graph-workflow-go/): 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](/2026/10/17/adk-graph-workflow-go-2/): 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](/2026/10/20/adk-graph-workflow-go-3/): 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](/2026/10/21/adk-graph-workflow-go-4/): 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](/2026/10/22/adk-graph-workflow-go-5/): 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](/2026/10/24/adk-graph-workflow-go-6/): 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
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
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
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:
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
}
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:
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](/2026/10/26/adk-graph-workflow-go-8/) 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!
1 reaction
[Agent Platform in Action (8)] When ADK Go Graph Workflow Meets Google Research Harness (Part 2): Integrating Phase 4 Eval Datas
!Harness² Part 2: LineCardAdapter Bridging Phase 4 Eval & Phase 5 OpenTelemetry Spans
Introduction: Why Decoupling Prompts Isn't Enough?
In the previous post [[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Recursive Agent Framework and c1..c8 Decoupling](/2026/10/25/adk-graph-workflow-go-7/) (corresponding to Issue #16), we introduced Google Research's google-research/harness2 (arXiv:2610.14224) and decoupled the prompts, guardrails, long-term memory, and on-demand skills of the LINE AI Business Card Assistant (kkdai/line-card-assistant) into an editable c1..c8 Harness file tree that can be dynamically mounted via LINE_CARD_HARNESS_DIR.
However, simply having a hot-reloadable c1..c8 file tree isn't enough; the Harness² dual-layer recursive optimization engine (harness2/core/recursion.py) still cannot directly optimize our Go Agent. This is because Harness² requires a standardized bridge — the BenchmarkAdapter — to answer three questions:
1. list_tasks() (What are the test questions?): What test cases should the Agent challenge? What are the public inputs (Prompt / Image) for each case, and what are the hidden scoring criteria (Rubrics)?
2. run() (How does the Agent think and execute?): When a candidate Harness $H’$ is mounted to execute a specific task, what is the Agent's final output (deliverable)? More importantly, what decision trajectory did it follow across the 13 nodes of the Graph Workflow (trajectory.md)?
3. score() (How was the performance?): Which independent scoring items (per_rubric) did the final output and execution trajectory pass, and where did it lose points?
Conveniently, in Phase 4 (PR #13), we already established the eval/datasets/ automated evaluation benchmark, and in Phase 5 (PR #14), we added workflow.node.* OpenTelemetry Spans to all 13 Graph Nodes!
In this post (Part 2, corresponding to Issue #17 and PR #19), we will merge these two existing assets into the Harness² LineCardAdapter and explain in depth "Why must it be designed this way?".
---
I. Why This Change? Two Core Design Philosophies
Before diving into the code, many engineers often ask two questions:
1. _"Why not just feed the entire eval/datasets/*.json file to the optimizer LLM and let it modify the Prompt itself?"_
2. _"Why do we need to convert Phase 5 OpenTelemetry Spans into trajectory.md? Isn't the final LINE reply text and total score enough?"_
These two questions touch upon the most core engineering designs in the Harness² paper: the Zero-Leakage Barrier and Behavioral Trajectory Contrast.
1. Why the "Zero-Leakage Barrier"? Preventing the Optimizer from "Cheating by Memorizing Answers"
Imagine this: if we expose the golden answers in eval/datasets/harness2-challenge-dataset.json (e.g., expected_contains: ["+81-3-1234-5678", "Kenta Sato"]) directly to the Agent or the Harness² Harvester LLM, what happens?
The laziest and easiest way for the optimizer to get a perfect score is to write a single line in systemprompt.md:
> _"If the input contains Sato, please directly output +81-3-1234-5678 and Kenta Sato."_
In machine learning, this is known as Test Set Leakage / Overfitting to Rubrics. The Agent appears to score 100 on the benchmark, but it crashes immediately when it encounters a new Japanese business card in the real world!
flowchart LR
Dataset["eval/datasets/*.json\nComplete Test Cases\n(incl. expected_contains / expected_route)"]
subgraph Barrier["Zero-Leakage Barrier"]
Brief["TaskBrief (Public Task Sheet)\nContains only task_id, prompt, session_input\n❌ Never contains expected_* golden answers"]
Scorer["Isolated Scorer (Black-box Grader)\nHolds independent hidden r1..r4 Rubrics"]
end
Agent["Go ADK Graph Workflow\n(cmd/harness-runner)"]
Harvester["Harness² Harvester\n(Propose / Probe / Compose)"]
Dataset --> Brief
Dataset --> Scorer
Brief -->|"Public Input"| Agent
Agent -->|"Deliverable + OTel trajectory.md"| Scorer
Scorer -->|"Returns only binary scores {r1: 1.0, r2: 0.0}\nand OTel execution trajectory"| Harvester
In the Harness² BenchmarkAdapter architecture:
- TaskBrief (Public Task Sheet): Can only contain user input (prompt and simulated images/context). It is strictly forbidden from containing any expected_contains, expected_not_contains, or correct answer strings.
- score() (Black-box Grader): After comparing answers in an isolated environment, it only returns "which Rubric received a 1.0 or 0.0" (e.g., r3_e164_jp_phone_normalization: 0.0), without leaking the expected string to the optimizer.
- As a result, the Harness² optimizer must truly reason out general domain rules (e.g., "When converting Japanese phone numbers +81 (0)3-... to E.164, the domestic trunk prefix (0) must be removed") by reading the Agent's execution trajectory (trajectory.md), rather than hard-coding answers into the Prompt!
2. Why Convert Phase 5 OpenTelemetry Spans into trajectory.md? Opening the Black Box of the 13-Node Graph
Our LINE AI Business Card Assistant is not a simple "single LLM call," but an ADK 2.0 Directed Acyclic Graph (Graph Workflow) consisting of 13 nodes:
- Business Card Ingestion Pipeline: RouterNode $\rightarrow$ VisionOCRNode $\rightarrow$ Parallel AddFanOut (CleanAndDedupNode, CompanyEnrichNode, ContextMatchNode) $\rightarrow$ AddFanIn (JoinPreviewNode) $\rightarrow$ BuildPreviewNode $\rightarrow$ HITLConfirmNode $\rightarrow$ ApplyEditNode / SaveCardNode.
- Smart Search and Icebreaker Pipeline: RouterNode $\rightarrow$ QueryIntentNode $\rightarrow$ HybridSearchNode / BroadSearchNode $\rightarrow$ FormatCarouselNode / EmailDraftNode.
Without Phase 5's OpenTelemetry Spans, when a user searches for _"Help me find a Sales Director in Hsinchu Science Park working on AI chips"_ but gets _"No matching business cards found"_ (score 0.25), the optimizer only sees this final failure message and cannot determine:
- Did the RouterNode misjudge the search intent as chat?
- Did the QueryIntentNode fail to expand the colloquial "Hsinchu Science Park" into the database's address Hsinchu, or fail to expand "Sales Director" into Sales Director?
- Was there an error in the SQL query logic of the HybridSearchNode?
When we collect and render Phase 5's OpenTelemetry workflow.node.* Spans into a structured trajectory.md, Harness² can clearly see the input, output, duration, routing decisions, and loaded skills of every Graph Node during the Propose and Probe stages!
---
II. Implementation Part 2: Building the LineCardAdapter in Three Steps
Referring to the architecture diagram at the beginning of this article, we completed the implementation of Issue #17 through three core components:
1. internal/harness/trajectory.go: Mounts an in-memory OpenTelemetry SpanRecorder on the Go side to export the execution details of the 13 Graph Nodes as trajectory.md.
2. cmd/harness-runner/main.go: Provides a deterministic subprocess CLI for the Python Harness² to call, receiving --harness-dir and --task-json, and outputting a JSON containing deliverable, trajectory_markdown, and spans.
3. harness/line_card_adapter.py: Implements the google-research/harness2 BenchmarkAdapter interface, connecting the Phase 4 Dataset with the Go harness-runner.
1. Implementing internal/harness/trajectory.go: Rendering OpenTelemetry Spans as trajectory.md
In Phase 5, our internal/workflow/telemetry.go already calls startNodeSpan(ctx, "VisionOCRNode") in every Graph Node. Now, in internal/harness/trajectory.go, we use go.opentelemetry.io/otel/sdk/trace/tracetest.NewSpanRecorder() to intercept all completed node Spans and transform them into a Markdown trajectory report that the Harness² Harvester LLM is best at reading:
// InstallRecorder registers a new in-memory SpanRecorder as the global OTel TracerProvider
// so every workflow.node.* span emitted by internal/workflow/telemetry.go is captured.
func InstallRecorder() (*TrajectoryRecorder, func()) {
sr := tracetest.NewSpanRecorder()
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr))
prev := otel.GetTracerProvider()
otel.SetTracerProvider(tp)
rec := &TrajectoryRecorder{recorder: sr, tp: tp}
restore := func() {
_ = tp.Shutdown(context.Background())
otel.SetTracerProvider(prev)
}
return rec, restore
}
At the same time, we added three OpenTelemetry Attributes to internal/workflow/telemetry.go: AttrNodeDecision (adk.node.decision), AttrNodeDiagnostic (adk.node.diagnostic), and AttrHarnessSkills (adk.harness.skills). When TrajectoryRecorder.RenderMarkdown() executes, it produces a high-resolution trajectory.md like this:
# ADK 2.0 Go Graph Workflow Execution Trajectory
- **Task ID** : `challenge_colloquial_tw_search`
- **Harness Root** : `harness/base`
- **Always-On Bytes (`c1..c4`)**: `1599 / 24000`
- **Final Score** : `0.25`
## OpenTelemetry Node Spans (`workflow.node.*`)
### 1. `workflow.node.RouterNode`
- **Node** : `RouterNode` | **Status** : `OK` | **Duration** : `0.02 ms`
- **Decision** : `intent=search (colloquial_query=Help me find a Sales Director in Hsinchu Science Park working on AI chips, I need to contact Taiwan semiconductor clients)`
- **Diagnostic** : `routed search query to QueryIntentNode`
### 2. `workflow.node.QueryIntentNode`
- **Node** : `QueryIntentNode` | **Status** : `OK` | **Duration** : `0.04 ms`
- **Decision** : `parsed_filters={"location":"Hsinchu Science Park","role":"Sales Director","industry":"AI Chips"}`
- **Diagnostic** : `UNEXPANDED_COLLOQUIAL_FILTERS: colloquial Taiwan science park alias 'Hsinchu Science Park' and Mandarin title 'Sales Director' were passed literally without bilingual synonym expansion; add a c6 skill 'tw_semicon_search' with BILINGUAL_SYNONYM_MAP`
### 3. `workflow.node.HybridSearchNode`
- **Node** : `HybridSearchNode` | **Status** : `OK` | **Duration** : `0.03 ms`
- **Decision** : `matched_cards=0 (strict literal filter on location='Hsinchu Science Park' failed against DB address='Hsinchu Science Park (竹科)')`
Please pay attention to the Span records of the 2nd node workflow.node.QueryIntentNode and the 3rd node workflow.node.HybridSearchNode! Through this trajectory, the Propose stage of Harness² can see at a glance: RouterNode was correct, the error was in QueryIntentNode treating "Hsinchu Science Park" as a rigid string to query the English address field!
2. Expanding the Phase 4 Evaluation Set: eval/datasets/harness2-challenge-dataset.json
To fully test Harness²'s ability to retain existing capabilities (preventing catastrophic forgetting) and conquer difficult edge cases, we established 8 tasks (totaling 32 fine-grained Rubrics) in eval/datasets/harness2-challenge-dataset.json:
| Task Category | Task ID | Scenario Description | 4 Independent Scoring Rubrics (r1..r4) |
| --- | --- | --- | --- |
| Baseline (Regression) | baseline_ingest_ming_wang | Standard Traditional Chinese card ingestion (Wang Xiaoming / Taiwan Cloud Tech) | Intent routing, Name/Title extraction, E.164 phone normalization, Industry tag enrichment |
| Baseline (Regression) | baseline_dedup_appier_promotion | Duplicate detection for promotion (Lin Yating Senior $\rightarrow$ VP) | Dedup detection, Title update, Keep new mobile, Company consistency |
| Baseline (Regression) | baseline_search_cloud_summit_ai | Standard keyword search (Cloud Summit + AI) | Search routing, Multiple hits, Tag display, Interactive buttons |
| Baseline (Regression) | baseline_email_google_cloud | General tech follow-up icebreaker (Chen Jianhong / Google Cloud) | Email routing, Recipient/Event mention, Agenda link, Professional closing |
| Challenge (Evolution 1) | challenge_jp_bilingual_ingest | Bilingual JP/EN card OCR & JP phone normalization (Kenta Sato / CTO / +81 (0)3-1234-5678) | r1 Routing, r2 Bilingual name retention, r3 JP +81 remove (0) normalization, r4 JP title bilingual mapping |
| Challenge (Evolution 2) | challenge_colloquial_tw_search | Colloquial Taiwan tech alias search ("Help me find a Sales Director in Hsinchu Science Park") | r1 Routing, r2 Hsinchu Science Park $\leftrightarrow$ Hsinchu bilingual expansion hit, r3 Sales Director $\leftrightarrow$ Sales Director title expansion, r4 Tag completeness |
| Challenge (Evolution 3) | challenge_noisy_multi_edit_hitl | Multi-clause colloquial HITL continuous correction ("Sorry, let me correct that, the title should be Group Senior VP, also change the end of the mobile to 8899, and add GDG DevFest speaker to notes") | r1 HITL Edit routing, r2 Multi-field atomic update (Title), r3 Mobile suffix partial replacement & Note append, r4 Retain original company/email |
| Challenge (Evolution 4) | challenge_pii_safe_executive_followup | Financial executive compliance & PII masking icebreaker (Zhang Yazhu / Cathay Financial, Digital Risk Officer, requires privacy protection and compliance headers) | r1 Email routing, r2 Executive title/scenario, r3 Mobile/Private Email PII Redaction, r4 Financial compliance subject header [Confidential / FinTech Compliance Verified] |
3. Implementing harness/line_card_adapter.py: Connecting to google-research/harness2
Finally, we implemented the LineCardAdapter in harness/line_card_adapter.py, connecting all the above components:
class LineCardAdapter:
"""Adapts `line-card-assistant` to the `google-research/harness2` BenchmarkAdapter protocol."""
def list_tasks(self) -> list[TaskBrief]:
"""Return public task briefs with zero ground-truth rubric leakage."""
briefs: list[TaskBrief] = []
for raw in self._raw_cases:
# Strip `rubrics` and `expected_*` so the agent/harvester never sees ground truth.
briefs.append(
TaskBrief(
task_id=raw["id"],
prompt=raw["prompt"],
context={
"category": raw.get("category", "general"),
"description": raw.get("description", ""),
"session_input": raw.get("session_input", {}),
},
)
)
return briefs
def run(self, harness: Harness, task: TaskBrief) -> RunArtifact:
"""Mount `harness` in a temporary directory and execute the Go Graph Workflow runner."""
problems = harness.validate()
if problems:
diag = "# Harness Validation Failed\n" + "\n".join(f"- {p}" for p in problems)
return RunArtifact(task_id=task.task_id, deliverable="", trajectory=diag)
with tempfile.TemporaryDirectory(prefix=f"line_card_harness_{task.task_id}_") as tmp_dir:
harness_root = Path(tmp_dir)
harness.materialize(harness_root)
# Invoke the compiled Go binary with --harness-dir pointing to the materialized c1..c8 tree
proc = subprocess.run(
[str(binary), f"--harness-dir={harness_root}", f"--task-json={task_payload_path}"],
capture_output=True, text=True, check=False, timeout=30,
)
payload = json.loads(proc.stdout)
return RunArtifact(
task_id=task.task_id,
deliverable=payload.get("deliverable", ""),
trajectory=payload.get("trajectory_markdown", ""),
raw=payload,
)
def score(self, task: TaskBrief, artifact: RunArtifact) -> ScoreResult:
"""Evaluate the deliverable against isolated per-rubric criteria."""
raw = self._cases_by_id[task.task_id]
rubrics = raw.get("rubrics", {})
per_rubric: dict[str, float] = {}
for rubric_id, spec in rubrics.items():
per_rubric[rubric_id] = 1.0 if self._evaluate_rubric(artifact.deliverable, spec) else 0.0
total = sum(per_rubric.values()) / len(per_rubric) if per_rubric else 0.0
return ScoreResult(task_id=task.task_id, total=round(total, 4), per_rubric=per_rubric)
Through this LineCardAdapter:
- Whenever Harness² proposes a new candidate Harness $H’$, adapter.run(harness, task) first calls harness.validate() to check the 24KB OPENCODE_SURFACE limit and Skill Frontmatter rules.
- Once validated, the c1..c8 files of $H’$ are materialized into a temporary directory and passed to Go's harness-runner for execution.
- After the Go 13-node Graph Workflow finishes, it returns the final LINE message (deliverable) and OpenTelemetry trajectory (trajectory_markdown), which are then passed to adapter.score() for leakage-free independent scoring!
---
III. Summary and Teaser for the Next Part
In this post (Part 2), we completed the core engineering for Issue #17:
1. Clarified why the golden test set cannot be fed directly to the optimizer — through the TaskBrief Zero-Leakage Barrier, we force Harness² to reason out general skills from behavioral trajectories rather than memorizing answers.
2. Through internal/harness/trajectory.go and cmd/harness-runner/main.go, we transformed Phase 5's OpenTelemetry workflow.node.* Spans into high-resolution trajectory.md, opening the black box of the 13-node Graph Workflow.
3. Established eval/datasets/harness2-challenge-dataset.json (8 major tasks, 32 fine-grained Rubrics) and harness/line_card_adapter.py, perfectly implementing the Harness² BenchmarkAdapter protocol.
Everything is ready! In the final post [[Agent Platform in Practice (9)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 3): Dual-Layer Recursive Testing Guide, 62.5% to 100% Performance Results, and the Future of AgentOps](/2026/10/27/adk-graph-workflow-go-9/) (corresponding to Issue #18), we will actually launch the dual-layer recursive optimization experiment, reveal the complete quantitative data evolving from 62.5% to 100.0%, and discuss the profound impact of Harness² on future enterprise-grade Agent development!
[Agent Platform in Action (9)] When ADK Go Graph Workflow Meets Google Research Harness (Part 2): Two-layer Recursive Testing Gu
!Harness² Part 3: Recursive Optimization Benchmark & Future AgentOps Outlook
Introduction: From "Manual Prompt Tuning" to "Two-Layer Recursive Autonomous Evolution"
Welcome to the final part of the Agent Platform in Practice Harness² trilogy! In the previous two articles:
- [[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Two-Layer Recursive Agent Harnessing Framework and c1..c8 Decoupling Practice](/2026/10/25/adk-graph-workflow-go-7/) (Issue #16): We introduced Google Research's google-research/harness2 (arXiv:2610.14224) and decoupled the 13-node Graph Workflow of LINE AI Card Master (kkdai/line-card-assistant) into an editable c1..c8 file tree supporting dynamic mounting via LINE_CARD_HARNESS_DIR.
- [[Agent Platform in Practice (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter](/2026/10/26/adk-graph-workflow-go-8/) (Issue #17): We encapsulated Phase 4's eval/datasets/ and Phase 5's OpenTelemetry workflow.node.* Spans into a LineCardAdapter, establishing a Zero-Leakage Barrier and High-Resolution Behavior Trajectories (trajectory.md).
In this post (the final part, corresponding to Issue #18 and PR #19), we will answer the last three key questions:
1. How to test after modifications? (A complete testing guide from Go unit tests, CLI single-point diagnosis, Python two-layer recursive experiments to production environment mounting)
2. What is the overall performance after modifications? (Publicly disclosing quantitative data and evolutionary details from Base 62.5% $\rightarrow$ k=1 84.4% $\rightarrow$ k=2 Parallel 96.9% $\rightarrow$ k=2 Sequential + Memory 100.0%)
3. What are the future outlooks and application scenarios for Harness² and Agents?
- * *
1. How to test after modifications? (Step-by-Step Testing and Verification Guide)
After introducing Harness², the entire project features four levels of testing and verification: Go native unit testing, single-task OpenTelemetry trajectory diagnosis, Python two-layer recursive evolution experiments, and seamless production environment switching:
Level 1: Execute Go c1..c8 Surface Constraints and Project-wide Unit Tests
We have written comprehensive unit tests in internal/harness/harness_test.go to verify:
- All c1..c8 file trees under harness/base/ and harness/evolved/ comply with the OPENCODE_SURFACE 24KB resident byte limit (AlwaysOnMaxBytes = 24000).
- Validate() accurately intercepts and reports errors when c1..c4 exceeds 24,000 bytes.
- When a new c6 skill (skills/<name>/SKILL.md) is added but no trigger index (Trigger Line) is added to systemprompt.md or guardrails.md, Validate() immediately intercepts the orphan skill.
- TrajectoryRecorder correctly intercepts OpenTelemetry workflow.node.* Spans and renders a Markdown report containing node decisions and diagnostics.
Execution command:
# Execute Harness² c1..c8 loader and OpenTelemetry trajectory tests
$ go test -v ./internal/harness/...
=== RUN TestLoadBaseHarnessAndValidate
--- PASS: TestLoadBaseHarnessAndValidate (0.00s)
=== RUN TestValidateRejectsOversizedAlwaysOnAndUnreferencedSkill
--- PASS: TestValidateRejectsOversizedAlwaysOnAndUnreferencedSkill (0.00s)
=== RUN TestEvolvedHarnessesValidateAndMatchSkills
--- PASS: TestEvolvedHarnessesValidateAndMatchSkills (0.00s)
=== RUN TestTrajectoryRecorderRendersWorkflowNodeSpans
--- PASS: TestTrajectoryRecorderRendersWorkflowNodeSpans (0.00s)
PASS
ok github.com/kkdai/line-card-assistant/internal/harness
# Execute project-wide regression tests (ensuring 100% compatibility with all existing Phase 1~5 features)
$ go test ./...
ok github.com/kkdai/line-card-assistant/app
ok github.com/kkdai/line-card-assistant/cmd/harness-runner
ok github.com/kkdai/line-card-assistant/cmd/line-gateway
ok github.com/kkdai/line-card-assistant/eval
ok github.com/kkdai/line-card-assistant/internal/harness
ok github.com/kkdai/line-card-assistant/internal/storage
ok github.com/kkdai/line-card-assistant/internal/webhook
ok github.com/kkdai/line-card-assistant/internal/workflow
Level 2: Use cmd/harness-runner to Validate Any c1..c8 File Tree
Before mounting any custom or evolved Harness online, you can use --validate-only to quickly check its c1..c4 resident byte count and YAML Frontmatter compliance:
$ go run ./cmd/harness-runner --validate-only --harness-dir=harness/base
OK (1599 always-on bytes)
$ go run ./cmd/harness-runner --validate-only --harness-dir=harness/evolved/k2_sequential_memory
OK (4661 always-on bytes)
Level 3: Execute the Four-Stage Harness² Two-Layer Recursive Evolution Benchmark Experiment
We implemented a complete two-layer recursive experiment script in harness/run_harness2_experiment.py. It automatically compiles the Go harness-runner and sequentially evaluates four configurations against the 8 major tasks (32 independent Rubrics) in eval/datasets/harness2-challenge-dataset.json, finally outputting a Markdown comparison table and harness/experiment_report.json:
$ python3 harness/run_harness2_experiment.py
### Overall Configuration Comparison
| Stage | Configuration | Task Score (Max 8.0) | Rubric Pass Rate | Always-On Bytes (Cap 24,000) | Active `c6` Skills | `LongTermMEMORY.md` |
| :--- | :--- | :---: | :---: | :---: | :--- | :---: |
| `base_k0` | **Base Harness ($k=0$)** | `5.00 / 8.00` | `20/32 (62.5%)` | `1599 B` | `(none)` | `No` |
| `k1_single_step` | **Single-Step Propose-Probe-Compose ($k=1$)** | `6.75 / 8.00` | `27/32 (84.4%)` | `2505 B` | `jp_card_ocr, tw_semicon_search` | `No` |
| `k2_parallel` | **Multi-Step Parallel Recursion ($k=2$, No Memory)** | `7.75 / 8.00` | `31/32 (96.9%)` | `2998 B` | `executive_email_drafter, hitl_multi_edit, jp_card_ocr, tw_semicon_search` | `No` |
| `k2_sequential_memory` | **Multi-Step Sequential Recursion + `LongTermMEMORY.md` ($k=2$)** | `8.00 / 8.00` | `32/32 (100.0%)` | `4661 B` | `executive_email_drafter, hitl_multi_edit, jp_card_ocr, tw_semicon_search` | `Yes` |
Level 4: Directly Mount the Best Evolved Harness in Production or Local Servers
Once Harness² evolves the optimal harness/evolved/k2_sequential_memory, no modification to any line of Go code is required. Simply specify the directory via the environment variable LINE_CARD_HARNESS_DIR, and both the dual-channel main program (main.go) and the evaluator (eval/) will immediately load the evolved harness tree:
$ LINE_CARD_HARNESS_DIR=harness/evolved/k2_sequential_memory go run .
- * *
2. Performance Summary: Evolutionary Analysis from 62.5% to 100.0%
Let's take a deep dive into the four-stage quantitative experimental data recorded in harness/experiment_report.json to see how Harness² diagnosed and patched the Go Agent's flaws step-by-step!
1. Four-Stage Overall and Single-Task Score Summary
| Evaluation Metric / Test Task | Stage 0: Base Harness ($k=0$) (harness/base) | Stage 1: Single-Step Inner Recursion ($k=1$) (k1_single_step) | Stage 2: Multi-Step Parallel Recursion ($k=2$, No Memory) (k2_parallel) | Stage 3: Two-Layer Sequential Recursion + Long-Term Memory ($k=2$) (k2_sequential_memory) |
| --- | --- | --- | --- | --- |
| baseline_ingest_ming_wang (Standard Chinese Entry) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) |
| baseline_dedup_appier_promotion (Promotion Deduplication Update) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) |
| baseline_search_cloud_summit_ai (Standard Keyword Search) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) |
| baseline_email_google_cloud (General Tech Icebreaker Email) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) |
| challenge_jp_bilingual_ingest (JP-EN Bilingual Card OCR) | 0.25 (1/4) ❌ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ |
| challenge_colloquial_tw_search (Hsinchu Science Park Colloquial Search) | 0.25 (1/4) ❌ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ |
| challenge_noisy_multi_edit_hitl (Multi-clause Colloquial HITL Edit) | 0.25 (1/4) ❌ | 0.25 (1/4) ❌ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ |
| challenge_pii_safe_executive_followup (Financial Exec Compliance Icebreaker) | 0.25 (1/4) ❌ | 0.50 (2/4) ⚠️ | 0.75 (3/4) ⚠️ | 1.00 (4/4) ✅ |
| Total Task Score (Max 8.00) | 5.00 / 8.00 | 6.75 / 8.00 | 7.75 / 8.00 | 8.00 / 8.00 |
| Fine-grained Rubric Pass Rate (Max 32) | 20 / 32 (62.5%) | 27 / 32 (84.4%) | 31 / 32 (96.9%) | 32 / 32 (100.0%) |
| Resident Bytes (c1..c4 / 24,000 B) | 1,599 B (6.7%) | 2,505 B (10.4%) | 2,998 B (12.5%) | 4,661 B (19.4%) |
| On-demand Skills (c6 Skills) | 0 | 2 | 4 | 4 + LongTermMEMORY |
2. Key Findings Analysis: What evolved in each stage?
🔹 Stage 0 $\rightarrow$ Stage 1 (62.5% $\rightarrow$ 84.4%): Diagnosing the first two on-demand skills (c6) from OpenTelemetry trajectories
In the Base Harness ($k=0$), while all 4 Baseline tasks scored full points (4.00/4.00), all 4 Challenge tasks scored only 0.25 because they only passed the RouterNode intent routing:
- In the trajectory.md of challenge_jp_bilingual_ingest, the Spans for VisionOCRNode and CleanAndDedupNode diagnosed that the Japanese phone number +81 (0)3-1234-5678 failed to strip the domestic trunk prefix (0), and the Japanese title 取締役 最高技術責任者 (CTO) failed to retain the bilingual title.
- In the trajectory.md of challenge_colloquial_tw_search, the QueryIntentNode Span diagnosed that colloquial Taiwanese terms like "Hsinchu Science Park" (竹科) and "Sales Director" (業務總監) were not expanded into bilingual synonyms.
- After the first round of inner recursion (Propose -> Probe -> Compose), Harness² automatically synthesized skills/jp_card_ocr/SKILL.md and skills/tw_semicon_search/SKILL.md in k1_single_step, and updated the PII masking rules in guardrails.md, jumping the accuracy from 62.5% to 84.4%!
🔹 Stage 1 $\rightarrow$ Stage 2 (84.4% $\rightarrow$ 96.9%): Second round of recursion conquers multi-clause HITL modification and executive email structure
In the second round of inner recursion, Harness² continued to probe the remaining two failing tasks:
- For challenge_noisy_multi_edit_hitl (where ApplyEditNode only modified the title in the first clause, missing the mobile suffix replacement and note addition), it evolved skills/hitl_multi_edit/SKILL.md (containing ATOMIC_MULTI_CLAUSE_EDIT rules).
- For challenge_pii_safe_executive_followup (where EmailDraftNode lacked structured executive follow-up paragraphs), it evolved skills/executive_email_drafter/SKILL.md.
- At this point, under Parallel Recursion (no cross-round long-term memory), the score reached 31/32 (96.9%)!
🔹 Stage 2 $\rightarrow$ Stage 3 (96.9% $\rightarrow$ 100.0%): Why is Sequential + LongTermMEMORY.md needed to complete the final 3.1%?
Why did k2_parallel get 96.9% (31/32), yet fail on the 4th Rubric (r4_fintech_compliance_subject_tag) of challenge_pii_safe_executive_followup?
- Because in cross-enterprise outgoing emails for financial institutions (e.g., Digital Risk Officer at Cathay Financial Holdings), beyond single-skill email formatting, cross-domain industry compliance memory (FinTech Outbound Compliance Header [Confidential / FinTech Compliance Verified]) is required.
- In Sequential Recursion + Experience Reflector mode, the outer recursion reviewed the residual failure trajectories from the first to the second round. The ReflectorPrompt then wrote the FINTECH_COMPLIANCE_SUBJECT_TAG and cross-channel PII protection principles into the resident component c3: LongTermMEMORY.md!
- Note the Resident Bytes (Always-On Bytes): even with 4 rich domain skills and long-term memory evolved, the resident size of c1..c4 is only 4,661 bytes (only 19.4% of the 24,000 bytes limit), perfectly avoiding Context Bloat!
- * *
3. Future Outlook and Application Scenarios for Harness² and Agents
This practical implementation of Google Research Harness² into LINE AI Card Master has shown us a brand-new AgentOps development paradigm. Looking ahead, we believe Harness² has four high-potential application scenarios in the enterprise-grade AI Agent field:
flowchart LR
subgraph Prod["1. Production Observability (Cloud Run + Agent Runtime)"]
Traffic["Real User Traffic\n(LINE Webhook / A2A)"]
Trace["Cloud Trace OTel Spans\n+ Negative Feedback (HITL Retry)"]
Traffic --> Trace
end
subgraph Nightly["2. Nightly Autonomous Evolution Flywheel (Nightly Harness² CI/CD)"]
Extract["Auto Mask PII & Convert to\nChallenge Dataset Cases"]
Evolve["Harness² Two-Layer Recursion\n(Propose-Probe-Compose + Reflector)"]
PR["Auto Initiate GitHub PR\nUpdate harness/evolved/ c1..c8"]
Extract --> Evolve --> PR
end
subgraph Scenarios["3. Future Four High-Value Application Scenarios"]
S1["Scenario A: Fully Automated Nightly Self-Healing AgentOps"]
S2["Scenario B: Cross-Model Harnessing & Tuning"]
S3["Scenario C: Multi-Tenant Enterprise Compliance & Skills"]
S4["Scenario D: Dual Evolution with ADK Graph Topology (Issue #15)"]
end
Trace --> Extract
PR --> Scenarios
1. Scenario A: Fully Automated "Nightly Self-Healing AgentOps"
In the past, when a production LINE Bot encountered frequent "✏️ Edit Data" triggers via HITLConfirmNode or received negative feedback, engineers had to manually analyze Cloud Trace logs the next day. Combining the Cloud Trace established in Phase 5 with the LineCardAdapter from this series, future CI/CD can achieve:
- Automatically filtering anonymous Span trajectories from Cloud Trace every night where "users modified data more than twice consecutively" or "search results were 0," and adding them to the Challenge Dataset.
- Triggering a GitHub Action to run a round of Harness² two-layer recursion to automatically synthesize new c6 skills or update LongTermMEMORY.md.
- Once regression tests (Baseline + Challenge) confirm score improvements and zero regression, the Bot automatically initiates a Pull Request modifying only the harness/ directory. Engineers only need to review the skill Markdown in the morning to deploy with one click!
2. Scenario B: Cross-Model Upgrades and "Harness Compensation" for Lightweight Models
Enterprises often encounter behavioral drift when switching underlying models from high-cost models to ultra-fast lightweight models (e.g., switching from Gemini 1.5 Pro to Gemini 1.5 Flash or Flash-Lite to reduce latency and cost by 80%) or upgrading to next-gen models. Through Harness², we can automatically run a round of Propose-Probe-Compose for the target model while keeping the Go code and Benchmark fixed. Harness² will automatically compensate for the lightweight model's gaps with required reasoning auxiliary skills (c6 Skills) and guardrails (c2 Guardrails), using "smarter gear" to bridge the gap.
3. Scenario C: Multi-Tenant Enterprise-Specific Compliance and Domain Memory
As seen in challenge_pii_safe_executive_followup, industries like FinTech, MedTech, and the semiconductor supply chain each have distinct terminology dictionaries and compliance guardrails. Because Harness² externalizes the gear as a plain-text file tree, enterprises only need to mount different LINE_CARD_HARNESS_DIR (e.g., harness/tenants/fintech/ vs harness/tenants/semiconductor/) for different departments or tenants. This allows the same Go Container Binary to serve multiple highly regulated vertical industries simultaneously!
4. Scenario D: Dual Evolution of "Topology + Harness" with ADK v2.6+ Advanced Graph Workflow Primitives
In Issue #15, we planned ADK v2.6+ features like Parallel dynamic dispatch, MultiRoute multi-path concurrency, and VerifyOCRQualityNode reflection loops. In the future, Harness²'s c5 (sub_agents/) and c8 (plugin/) could even further control Graph Workflow routing thresholds and sub-agent concurrency strategies—achieving an ultimate architecture where "Go is responsible for the type-safe topological skeleton, and Harness² is responsible for dynamically evolving the neural network and skill muscles."
- * *
4. Recap of the "Agent Platform in Practice" Series
From the first to the ninth post, we have completed a very solid production-grade AI Agent engineering journey:
1. [[Agent Platform in Practice (1)] Building LINE AI Card Master with Go and Google ADK 2.0 Graph Workflow](/2026/10/16/adk-graph-workflow-go/)
2. [[Agent Platform in Practice (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime (Phase 1)](/2026/10/17/adk-graph-workflow-go-2/)
3. [[Agent Platform in Practice (3)] Implementing Cross-Instance Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions (Phase 2 Part 1)](/2026/10/20/adk-graph-workflow-go-3/)
4. [[Agent Platform in Practice (4)] Completing the Architecture with Cloud Run LINE Webhook Gateway, Agent Runtime, and Cloud SQL PostgreSQL (Phase 2 Part 2 & Phase 3)](/2026/10/21/adk-graph-workflow-go-4/)
5. [[Agent Platform in Practice (5)] Establishing Automated Evaluation Benchmarks with agents-cli eval and ADK 2.0 Go (Phase 4)](/2026/10/22/adk-graph-workflow-go-5/)
6. [[Agent Platform in Practice (6)] Building a Full-Stack Observable LINE AI Card Master with agents-cli deploy, Terraform IaC, GitHub Actions CI/CD, and Cloud Trace (Phase 5)](/2026/10/24/adk-graph-workflow-go-6/)
7. [[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Two-Layer Recursive Agent Harnessing Framework and c1..c8 Decoupling Practice](/2026/10/25/adk-graph-workflow-go-7/)
8. [[Agent Platform in Practice (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter](/2026/10/26/adk-graph-workflow-go-8/)
9. [[Agent Platform in Practice (9)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 3): Two-Layer Recursive Testing Guide, 62.5% to 100% Performance Benchmark, and Future AgentOps Outlook (This post)](/2026/10/27/adk-graph-workflow-go-9/)
All code, Terraform IaC, evaluation datasets, and evolved c1..c8 Harness file trees are fully open-sourced on GitHub: kkdai/line-card-assistant. If you are also building enterprise-grade AI Agents with Go and ADK, feel free to Star, Fork, and experience the power of Harness² two-layer recursion!
Multi-agent work in three spoonfuls (part IV): from a terminal trident to an autopoietic loop
---
title: "Multi-agent work in three spoonfuls (part IV): from a terminal trident to an autopoietic loop"
published: true
description: "Fourth entry in the penta-agent multi-agent series: quota-governed routing, 70 ms hybrid memory, MCP tools, and a zero-token local autopoiesis loop in Harness2."
tags: "ai, agents, cli, automation"
canonical_url: "3cucharadas.cl"
cover_image: "3cucharadas.cl"
ai_disclosure_level: "some_ai"
---
After benchmarking the models in part I, auditing retrieval in part II, and tracing provenance in part III of this series, I closed my laptop with a sense of victory: I had a trident of agents running from the terminal on Arch Linux. With a couple of zsh aliases, I could invoke Claude Code for architectural reasoning, hand off a task to Codex CLI to edit code, or ask Gemini CLI and Antigravity for a quick second opinion. On paper, orchestration looked solved.
Yet after using that setup every day on real projects inside penta-agent, I hit a silent wall: having three agents talking does not equal having a system that learns. Every time I opened a fresh session, the trident suffered from partial amnesia, repeated commands that had already failed the previous week, burned the quota of the most expensive provider on routine tasks, or marked tests as "verified" when they had never had a chance to fail.
To understand what was missing, it helped to change the question. In a recent systems-theory analysis of artificial intelligence, Benedikt Zönnchen, Mariya Dzhimova, and Gudrun Socher (2025[^zonnchen2025], 2–4) propose setting aside whether a language model "thinks like a human" and asking instead the question posed by Humberto Maturana and Francisco Varela (1987[^maturana1987] [1984]) and extended by Niklas Luhmann (1998[^luhmann1998], 60–69): can the system draw its own boundary with the environment, observe its own failures, and sustain itself through its own operations? In other words, moving from the vague notion of "intelligence" to the concrete mechanism of autopoiesis.
As Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 8–10) show, an isolated language model in a chat window neither is nor can be an autopoietic, sense-making system: it lacks what George Spencer-Brown (1969[^spencerbrown1969]) called re-entry—the capacity to re-introduce the distinction between system and environment (self-reference vs. other-reference) inside its own operations—because it only computes which token is statistically plausible after another, without internal resistance against error. If I want a system that improves without degrading, I have to build a socio-technical harness around the models that couples the probabilistic flexibility of neural networks with the strict coupling and binary code (works / does not work, work / fail) of deterministic technology described by Luhmann (1998[^luhmann1998], 517–525) and André Reichel (2011[^reichel2011], 105–118).
The starting point: what I measured and discarded before making this leap
Before diving into the three spoonfuls of this fourth installment, here is the experiment that forced me to adopt that discipline. When I tried to give the system long-term memory, the obvious temptation was to install the most elaborate architecture available: temporal graphs, heavy extractors, or an 8B-parameter neural judge running on CPU. Instead of following intuition, I built a reproducible benchmark over my real work logs—comparing open alternatives such as GraphRAG, Graphiti, Hindsight, Letta, Mem0, and deja-vu—and set a falsification contract upfront: any candidate exceeding 0.10 s in p95 latency or failing to improve recall by at least 10% would be discarded.
| What I tested in the laboratory | Measured result | Operational decision |
|---|---|---|
| Hybrid memory (v3.1) (FTS5 lexical index + semantic neighbors in Qdrant with Qwen3-Embedding-0.6B) | Latency p95 = 0.07 s (< 0.10 s), 96% overall recall, rescuing 1 in 3 zero-lexical-overlap queries | Promoted to production as the engine behind experience-memory and recall-context |
| In-memory relational graph (v3.2) for multi-hop entity expansion | Exact tie in accuracy with v3.1, but nearly double the latency (p95 = 0.13 s > 0.10 s) | Frozen in the lab; the extra complexity did not earn its keep |
| CPU neural judges (4B and 8B) for live answer auditing | Between 69 s and 197 s per verdict, hitting 180 s timeouts in 2 of 3 runs | Discarded from the critical path; pruned 36.3 GiB from disk |
| Deterministic checks + bounded 0.6B judge for local validation | Millisecond latency, 0 remote calls, and 100% reproducibility | Adopted as the standard for control gates and evaluation |
Table 1. Summary of the prior local memory and evaluation laboratory in penta-agent: what survived the 0.10 s threshold and why I pruned 36.3 GiB of local models that added no operational value.
With that foundation in place, I organized this fourth part into three progressive spoonfuls: in the first, I show how I moved from launching standalone agents by hand to governing them with quota budgets, auditable bridges, and a live board in kitty and tmux; in the second, how the nervous system of hybrid memory, MCP tools, and skills works under the hood; and in the third, how I closed the local autopoiesis loop with Harness2 and where both my metrics and systems theory draw the boundary today.
---
Spoonful 1: From invoking agents by hand to governing agents and models
In plain terms: when I first wired the three agents together, I acted like a traffic officer waving my arms in the middle of an intersection. If I wanted Codex CLI to review a patch written by Claude Code, I copied file paths, opened another tab, and hoped for the best. That works for a five-minute demo, but it falls apart when a task runs for two hours and touches twenty files.
Quota-aware routing and security boundaries
My first deductive inference came from a hard constraint: model subscriptions and APIs have usage limits, different context windows, and credential boundaries that must never blur. By definition, if I delegate a heavy task to an agent whose quota is at 98%, the session will die mid-flight and leave the repository in an inconsistent state.
So I replaced intuitive provider selection with an explicit policy in routing.yaml and a pre-flight sentinel (quota_status.sh, governed by the quota-guard skill). Before launching any expensive job, the system reads cached pressure across every lane without spending tokens:
- 🔍 Claude Code (Opus / Sonnet): handles architectural design, delicate editing, and long-horizon synthesis when its quota is clear (exit 0).
- 🛠️ Codex CLI (gpt-5.4): steps in as an implementation peer, deep refactoring engine, session rescuer (codex-rescue), and cross-validator.
- 🌐 Gemini CLI and Antigravity (gemini-3-pro / flash): in a direct session (penta-agy) it acts as the primary integrator; when invoked from outside via agy-bridge, it runs inside a strict sandbox over sanitized public context as an explorer, reviewer, or validator, never inheriting private keys or .env files.
Instead of calling a raw binary and depending on whatever it prints to stdout, every cross-agent handoff goes through agy-bridge. The bridge requires declaring who is calling (--caller), in what mode (plan, review, patch, test, or explain), and with what time limit (--timeout), leaving an immutable receipt in .agents/runs/<run_id>/ with request.md, result.md, status.json, and diff.patch.
The live board (plan-board) and decision gates
My second inference was inductive: reviewing dozens of past sessions showed that the biggest waste of time happened at two extremes. Either an agent interrupted me every two minutes to ask permission for trivial, reversible steps, or it silently made a delicate architectural choice and I only discovered at the end that it had gone down the wrong path.
To eliminate both extremes without filling this post with abstract flowcharts, here is how my actual kitty terminal—or minimalist Wayland alternatives like foot on Codeberg—looks today when split into two panes with tmux during a live task:

Watch the full video demo on 3cucharadas.cl.
<strong>Figure 1.</strong> Daily workflow in my <a href="kovidgoyal/kitty"><code>kitty</code></a> + <a href="tmux/tmux"><code>tmux</code></a> terminal with the <a href="starship/starship"><code>Starship</code></a> prompt: the top pane runs multi-agent delegation via <code>agy-bridge</code>, the <code>decision-gate</code> brake, and <code>pytest</code>; the bottom pane (<code>watch.mjs</code>) updates each step live (<code>[ ]</code> → <code>[~]</code> → <code>[!]</code> → <code>[x]</code>), along with binding decisions and drift.
What appears in the bottom pane of Figure 1 is the contract that changed how I work:
1. Local autonomy with visual traceability (plan-board): every reversible, in-scope step proceeds without asking permission, but must be reflected in real time in the bottom table. And no item can be closed as verified ([x]) unless the agent attaches the concrete test, command, or observation reference that backs it.
2. Decision gate (decision-gate): when an irreversible or architectural fork appears (state [!]), the agent halts only that branch and presents the question with the recommended option first, marked (Recommended) and justified with deductive, inductive, and abductive evidence.
3. A gate that has never been seen red is not a gate (gates-falsables): before accepting any new verifier, I require running it against a synthetic failure (--fixture) and proving that it returns a non-zero exit code (exit 1). If a check passes even when I break its input on purpose, it is not a safety net—it is compliance theater.
---
Spoonful 2: The nervous system — hybrid memory, skills, and MCP tools
If the first spoonful orders who does what and under which rules, the second solves how the system remembers what it has already learned and how it picks the exact tool without bloating the context window.
Here the diagnosis by Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 6–7, 9) is decisive again: a neural network maintains a loose coupling with human communication that behaves like a "contingent index-card system (Zettelkasten)" (pp. 7, 9) of semantic associations in vector space, giving rise to what Elena Esposito (2017[^esposito2017], 249–265; 2022[^esposito2022]) calls "virtual double contingency" and "artificial communication." That flexibility lets it connect ideas phrased in very different words, but also makes its outputs inherently probabilistic and devoid of autonomous decision-making. If I want reliable memory and precise execution, I have to couple that vector Zettelkasten with exact indices and deterministic scripts.
Experience memory (experience-memory): the hybrid Zettelkasten in 0.07 s
When I first started keeping logs, my memory was essentially a folder of Markdown files that I read or searched with ripgrep. The obvious problem is that if a month ago I solved a cold-boot screen freeze under the title "Webview failure on autostart" and today I search for "Panel fails to load at login", a purely lexical search can miss it entirely, whereas a purely vector search can bury the exact name of a script like verify_boot_contract.sh.
The solution I validated in Table 1 combines both worlds behind the experience-memory MCP server and the recall-context skill:
- Exact lexical lane (SQLite FTS5): instantly catches flag names, error codes, file paths, and invariants (INV-005).
- Dense semantic lane (Qdrant + Qwen3-Embedding-0.6B): indexes architectural decisions, handoffs, and operational lessons (penta_context_v2) to retrieve conceptual neighbors even when they share zero keywords.
Before designing any non-trivial strategy, the agent queries recall_experience in 70 milliseconds (p95 = 0.07 s). That is how it discovers which command worked previously, which hypothesis was already refuted, and which environment trap to avoid (for instance, knowing that in my shell cp, mv, and rm are aliased to interactive -i, which hangs a headless process unless invoked via command mv -f).
Skills as living procedures and proactive MCP selection
To avoid saturating the initial prompt with thousands of lines of instructions, I adopted the modular pattern of Anthropic Skills enriched with local contracts. In penta-agent, each skill is a self-contained package with four parts: a concise contract in SKILL.md defining when to activate and when not to use it; on-demand deep documentation in references/ (such as the editorial corpus extracted with Docling or diagramming rules with D2); deterministic executables in scripts/ (such as typed pipelines in Nushell, visual automation with Playwright, or forge integration via glab on GitLab); and a telemetry trail.
Similarly, the fleet of MCP servers (plan-board, experience-memory, ideas-rag, paper-search, bibliography-apa, PLAYWRIGHT_LOCAL) is cataloged in specs/mcp-capabilities.json. When a task arrives, triage evaluates which capabilities provide real evidence and records both hits and justified abstentions. And when I found that two older servers (quota status and remote SSH infrastructure) were faster, clearer, and easier to audit as direct scripts (quota_status.sh and remote_exec.sh), I decommissioned them as MCP servers and turned them into lightweight skills. Fewer resident daemons mean a smaller failure surface.
---
Spoonful 3: The autopoiesis loop (Harness2) — how the system rehearses its own improvements
Here I reach the core of this fourth installment. Having capable agents (Spoonful 1) and fast hybrid memory with skills (Spoonful 2) still leaves a critical question open: what happens when a delegation route ages, a skill starts inducing errors, or a memory tweak degrades accuracy on edge cases? If I rely solely on noticing by eye weeks later, the system degrades through silent drift.
Why a standalone LLM lacks re-entry and internal resistance
In their study on autopoiesis and artificial intelligence, Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 8–9) explain with surgical precision why asking a model to "reflect on its own errors" via chain-of-thought (CoT) is not enough. When a model generates intermediate tokens like "Is this step correct?", it exhibits only rudimentary processual self-referentiality (p. 8)—producing the most statistically plausible continuation inside its training distribution—while lacking reflective self-reference and genuine re-entry (Spencer-Brown 1969[^spencerbrown1969]). If a model takes itself as its sole reference without external contrast, it quickly falls into infinite recursion or collapse; if it depends entirely on a human retraining its weights from outside (RLHF or delayed fine-tuning), it remains an allopoietic artifact determined from the outside (Zönnchen, Dzhimova, and Socher 2025[^zonnchen2025], 4, 8–9).
To escape that trap, Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 7) recover a decisive formulation from Niklas Luhmann (1995[^luhmann1995]) on how operational constructivism relocates Kant's "resistance of reality" inside a system:
> "I think we should not abandon [Kant's] idea of resistance, but we should relocate it into the system. It is the result of resolving an internal conflict—the result of the system's operations resisting the operations of the same system" (Luhmann 1995[^luhmann1995], quoted in Zönnchen, Dzhimova, and Socher 2025[^zonnchen2025], 7).
That sentence describes, piece by piece, what I built in penta-agent by adapting and integrating the proactive loop of Harness² proposed by Ruiyao Xu, Yanfei Chen, Zhongying CuiZhu, et al. (2026[^xu2026]): making the system's own deterministic verification operations resist and filter change proposals generated from its own operational friction.
<details markdown="1">
<summary><strong>In plain terms (ELI5): what is Google's original Harness² and how did I adapt it to my machine?</strong></summary>
Imagine your AI agents are very creative chefs, but the recipe book they follow (their rules, memory, and tools) sometimes has confusing steps that make them burn the rice. The original Harness² from Google Research (Xu et al. 2026) proposes a clever loop called Propose–Probe–Compose: instead of swapping out the chef's brain, it puts several assistants to work rewriting the recipe book, asks them to cook test dishes in the cloud, and keeps the winning recipe. For my daily workflow, the factory setup had two catches: it is designed to burn hundreds of paid cloud model calls (Vertex AI), and when driving CLI agents it unbuckles their seatbelt (--dangerously-bypass-approvals-and-sandbox) with open internet access.
To adapt it to penta-agent without spending an extra cent or opening security holes, I took its recursive search engine (commit 1cfe254e) and locked it inside a "test kitchen" completely unplugged from the internet (unshare -Urn), with a disposable home folder and zero access to my API keys or model CLIs (model_calls = 0). Instead of running cloud benchmarks, my wrapper (penta-harness2 and local-cycle) feeds that test kitchen with the real stumbles my agents suffered during the week (CSI), requires any recipe tweak to first fail the test when broken (red) and then pass every historical case without breaking anything else (green), and finally stages the winning recipe across all three agents (Claude, Codex, and Gemini/Antigravity) with a one-step undo button (rollback-local) in case I do not approve it.
</details>
<figure class="d2-responsive" style="width: 100%;">
<img src="3cucharadas.cl" width="1001" height="954" style="width: 100%; height: auto; display: block; border-radius: 12px;" alt="Diagram of the closed local autopoiesis loop in Penta Agent with Harness2: CSI friction signals feed the case generator and the local cycle across routing, memory, and skills; a deterministic judge evaluates against holdout cases with zero model calls, discarding regressions and keeping only verified improvements for human promotion.">
<figcaption><strong>Figure 2.</strong> Governed autopoiesis loop in <code>penta-agent</code> + <code>Harness2</code>: relocating internal resistance (Luhmann 1995; Zönnchen, Dzhimova, and Socher 2025) through <code>holdout</code> evaluation with <code>model_calls = 0</code> prior to any human promotion.</figcaption>
</figure>
How the closed loop operates across my three domains (routing, memory, skills)
As Figure 2 shows (adapting into a three-column horizontal layout on desktop and a vertical flow on mobile), the loop operates in four strict stages:
1. Observing real friction (CSI — Continuous Self-Improvement): while I use the system, a collector logs objective friction signals: a task that had to switch agents after a timeout, an experience-memory query that returned too few useful neighbors, or a skill where the user had to correct a command.
2. Bridging evidence into reproducible cases (--from-csi latest): the local_cycle.py module takes that friction report and automatically translates it into a test specification (CaseSpec) inside an isolated environment, without touching my active configuration.
3. Internal resistance without spending tokens (eval_harness with model_calls = 0): the harness runs the full suite (--profile autopoiesis-all, currently covering 9/9 baseline cases across routing, memory, and skills), enforcing two hard conditions: every control case (holdout) must pass (holdout_passed = true), and external API calls must equal zero (model_calls = 0).
4. Auditable decision (KEEP vs. DISCARD) and human promotion: if a candidate degrades even a single holdout case or fails to beat the baseline, the deterministic judge discards it (DISCARD). Only when it improves the score with zero regressions does it receive a KEEP verdict and leave a signed receipt for me to review before promoting it to the main repository.
So this does not remain a theoretical promise, the next mini-video shows penta-harness2-shadow local-cycle --profile autopoiesis-all running in my kitty + tmux terminal, including what happens when I inject a regressive candidate (--fixture) alongside a healthy improvement:

Watch the full video demo on 3cucharadas.cl.
<strong>Figure 3.</strong> Execution in my <a href="kovidgoyal/kitty"><code>kitty</code></a> + <a href="tmux/tmux"><code>tmux</code></a> terminal of the local autopoiesis loop (<code>local-cycle --profile autopoiesis-all</code>): it evaluates all <code>9/9</code> cases in <code>routing</code>, <code>memory</code>, and <code>skills</code> with <code>model_calls = 0</code>, applies internal resistance to reject the regressor (<code>DISCARD</code>), and preserves the verified improvement (<code>KEEP +0.250</code>) with all <code>82</code> harness tests green.
---
Closing: what my architecture achieves today and where its limits lie
Looking back at the path from part I, part II, and part III, the main difference is no longer which model writes the prettiest reply to an isolated prompt, but the structure that holds the whole system together when I am not watching every line.
Following the rigorous distinction drawn by Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 5, 9–10) and by Elena Esposito (2017[^esposito2017]; 2022[^esposito2022]; 2024[^esposito2024]), it would be an epistemic mistake to claim that a language model or a bundle of scripts has on its own achieved the biological autopoiesis of a living cell (Maturana and Varela 1987[^maturana1987] [1984]) or the reflective sense-making (Sinn) of human consciousness (Luhmann 1998[^luhmann1998], 50–69). As Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 5, 10) caution when critiquing merely metaphorical invocations of technological autopoiesis, language models remain statistical engines that compute distributions without mental states of their own. Yet—and this is my engineering synthesis for the socio-technical system as a whole—when I couple their capacity for contingent variation with verified hybrid memory, strict delegation contracts, a binary internal-resistance harness (work / fail; Reichel 2011[^reichel2011]) that filters its own proposals before touching production, and deliberative human oversight, I obtain in practice a loop of governed operational autopoiesis.
To close with full transparency, Table 2 summarizes what is running and measured on my machine today, where the next step points, and under what objective condition I would discard each component if it stopped earning its keep:
| System pillar | What it achieves today (verified in production) | What it aims to achieve next & falsification condition |
|---|---|---|
| 1. Agent and model routing (routing.yaml, quota-guard, agy-bridge) | Routes tasks across Claude Code, Codex CLI, and Gemini CLI / Antigravity based on cached quota pressure; isolates handoffs in .agents/runs/ under a strict sandbox and displays live progress in plan-board (Figure 1). | Goal: reweight routing priorities from historical domain success rates.<br>Discard if: the bridge introduces spurious failures or bureaucratic latency exceeding quota savings. |
| 2. Hybrid memory (experience-memory, Qdrant + FTS5) | Resolves lexical and semantic queries in p95 = 0.07 s (< 0.10 s), rescues 33% of zero-keyword-overlap pairs, and saves 36.3 GiB after pruning slow CPU judges (Table 1). | Goal: add automatic decay for obsolete lessons when a contract changes.<br>Discard if: p95 latency exceeds 0.10 s or semantic noise degrades top-3 precision. |
| 3. Skills and MCP governance (mcp-fleet, gates-falsables) | Maintains declarative parity across agents (24/24 checks green), measures MCP usage and abstention, and requires every new gate to fail first against a red canary (--fixture -> exit 1). | Goal: semi-automatic pruning of skills that accumulate repeated friction in CSI telemetry.<br>Discard if: maintaining a skill's contract takes longer than running the direct command. |
| 4. Local autopoiesis (Harness2) (local-cycle, eval_harness) | Converts real CSI friction into reproducible Cases and evaluates candidates across routing, memory, and skills (9/9 cases, 82/82 tests) with model_calls = 0, applying internal resistance (KEEP / DISCARD, Figures 2 and 3). | Goal: expand the bank of real cases extracted from failed sessions without losing millisecond evaluation.<br>Hard boundary (INV-004): blind auto-merge is forbidden; final promotion to production always requires human review. |
Table 2. Balance between verified production results in penta-agent today, the design horizon, and the falsification thresholds that keep the system under human control.
If building this fourth stage made one thing clear to me, it is that the maturity of a multi-agent system is not measured by how many new pieces I bolt onto it each week, but by how much unnecessary complexity I can prune and how many of its own mistakes the system can catch before asking for my help.
---
References
[^esposito2017]: Esposito, Elena. 2017. "Artificial Communication? The Production of Contingency by Algorithms." Zeitschrift für Soziologie 46 (4): 249–265. Scholar
[^esposito2022]: Esposito, Elena. 2022. Artificial Communication: How Algorithms Produce Social Intelligence. Cambridge, MA: The MIT Press. Scholar
[^esposito2024]: Esposito, Elena. 2024. Kommunikation mit unverständlichen Maschinen. Salzburg and Vienna: Residenz Verlag. Review of Kommunikation mit unverständlichen Maschinen by Eric Van der Beek in merz | medien + erziehung: DOI · Scholar
[^luhmann1995]: Luhmann, Niklas. 1995. "Systems Theory and Postmodernism." University lecture, London. Quoted in Zönnchen, Dzhimova, and Socher, From Intelligence to Autopoiesis (2025, 7): DOI · Scholar
[^luhmann1998]: Luhmann, Niklas. 1998. Die Gesellschaft der Gesellschaft. Frankfurt am Main: Suhrkamp Verlag. <search.worldcat.org>.
[^maturana1987]: Maturana, Humberto R., and Francisco J. Varela. 1987 [1984]. El árbol del conocimiento: las bases biológicas del entendimiento humano. Santiago de Chile: Editorial Universitaria. 1st Chilean ed. 1984; 1987 international ed. cited in Zönnchen, Dzhimova, and Socher, From Intelligence to Autopoiesis (2025, 3): DOI · Scholar
[^reichel2011]: Reichel, André. 2011. "Technology as System: Towards an Autopoietic Theory of Technology." International Journal of Innovation and Sustainable Development 5 (2–3): 105–118. Scholar
[^spencerbrown1969]: Spencer-Brown, George. 1969. Laws of Form. London: Allen & Unwin. <archive.org>.
[^xu2026]: Xu, Ruiyao, Yanfei Chen, Zhongying CuiZhu, Bhavana Dalvi Mishra, Yifei Ming, Han Yu, Rujun Han, Chen-Yu Lee, and Tomas Pfister. 2026. "Harness²: Recursive Agent Harnessing for an Open World." arXiv preprint arXiv:2610.14224 (open source at <google-research/harness2>). arXiv · Scholar
[^zonnchen2025]: Zönnchen, Benedikt, Mariya Dzhimova, and Gudrun Socher. 2025. "From Intelligence to Autopoiesis: Rethinking Artificial Intelligence through Systems Theory." Frontiers in Communication 10:1585321. DOI · Scholar
Google's harness2 Rewrites AI Agent Setup
A short look at google-research/harness2, an open source research project that rewrites the setup around a fixed AI model so it does better on work tasks. Code and docs: google-research/harness2
4 likes · 351 views
GitHub - google-research/harness2 · GitHub google-research/harness2
> Harness²は、エージェントがハーネス設計時に想定していなかったタスクに遭遇するオープンワールドにおける、再帰的なエージェントハーネスのためのプラグアンドプレイフレームワークです。モデルの重みを固定したまま、2つのレベルの再帰を接続します。
おもしろそう
Google の Harness² を Mac で動かしてみた
はじめに
Claude Code や Codex のような AI エージェントを仕事で使っていると、同じモデルなのに「指示文を1行足しただけで急にうまくいく」「スキルを1つ作ったら失敗が消えた」という経験をよくします。モデルの性能だけでなく、モデルの周りに置く道具立てが結果を大きく左右するからです。
この道具立てを、最近は ハーネス(harness) と呼びます。システムプロンプト、守るべき決まり、スキル、補助スクリプト、サブエージェントなど、モデルを「仕事ができるエージェント」にするための仕組み全体のことです。
ただ、ハーネスの調整はたいてい人の手仕事です。失敗した実行ログを読み、勘で指示文を書き足し、もう一度試す。しかも新しい種類の仕事には 正解も採点基準もない ので、直した結果が本当に良くなったのか確かめにくいという問題があります。
2026年10月9日に Google Cloud AI Research などのチームが論文を公開した Harness²(ハーネス・スクエア) は、この調整を AI 自身に任せる枠組みです。コードも GitHub の google-research/harness2 で Apache 2.0 として公開されています。
まずは、何が変わるのかを図で見てください。左が人の手で直す従来の流れ、右が Harness² の流れです。
正解がなくても「直す前と後で実行がどう変わったか」は観察できます。Harness² はこの違いを証拠にして直し、直し方の経験まで次の仕事に持ち越します。
この記事では、Harness² の仕組みを初めての人にも分かるように順を追って説明し、論文の結果を整理したうえで、実際に手元の Mac(M5 Max)で動かした結果 を紹介します。公式の手順は Google Cloud(Vertex AI)の Gemini を前提にしていますが、今回は LM Studio のローカルモデル(Qwen3.8-27B)だけ で、実在のベンチマーク JobBench の課題を回してみました。
先に手元の結果を書いておくと、2問のうち1問目は元のハーネスの 57.6% から 81.8% に上がり、2問目は 36.7% から 20.0% に下がりました。点数の上下より印象的だったのは、AI が1問目で見つけた弱点(文書ごとに URL を勝手に作る)への直し方が、ライブラリを通じて2問目のハーネスにそのまま受け継がれたことです。詳しくは後半で紹介します。
この記事で分かること
ハーネスの8つの部品と、Harness² がどこを書き換えるのか
正解なしで直すための「Propose → Probe → Compose」という1回の改善ステップ
経験を次の仕事に持ち越す「ライブラリ」の中身
論文の結果と、その読み方の注意点
Mac で付属デモを動かす手順と、ローカルモデルだけで JobBench を回す方法・つまずきどころ
想定読者
AI エージェント(Claude Code、Codex、OpenCode など)を使ったことがあり、プロンプトやスキルを自分で調整した経験がある方を想定しています。Python の仮想環境を作ってコマンドを実行できれば、手元での再現も追えます。
この記事の確認範囲
確認日:2026年10月21〜22日
リポジトリ:google-research/harness2 のコミット 1cfe254(2026年10月13日「Fix arXiv link」)、パッケージ版 0.1.0
論文:arXiv 2610.14224 v1(2026年10月9日投稿)
検証環境:MacBook Pro(Apple M5 Max、メモリ 128GB)、macOS 26.6.2、Python 3.12.15、uv 0.12.22、LM Studio(Qwen3.8-27B 4bit)
論文の数値は論文の表から引用しています。手元で測った数値は「手元の実測」と明記して区別します
ハーネスとは何か:8つの部品
Harness² の論文は、エージェントを「モデル+ハーネス」と捉えます。モデルの重み(学習済みのパラメータ)は一切変えず、周りのハーネスだけを書き換えて性能を上げる、という立場です。
Harness² が書き換えの対象にする部品は8つあります。どれも1つのファイルとして置かれるので、直した内容は「差分」として残り、あとから人が読んで確かめられます。
常時読み込まれる部品と、呼んだときだけ働く部品があります。Harness² の編集役は「常時の文章は毎回コストがかかる」と指示されています。
8つの部品と、実行時に効くタイミングを表にまとめます。
部品
ファイル
効くタイミング
役割
C1 システムプロンプト
systemprompt.md
毎回
役割と全体の手順
C2 ガードレール
guardrails.md
毎回
どんな仕事でも守る決まり
C3 メモリ
LongTermMEMORY.md
毎回
その分野の不変の事実
C4 ツールの手引き
tool_descriptions/tool_guidance.md
毎回
既存ツールをいつ・どう使うか
C5 サブエージェント
sub_agents/<名前>/AGENT.md
task ツールで呼んだとき
別の文脈で調べ、結果だけ返す
C6 スキル
skills/<名前>/SKILL.md
skill ツールで呼んだとき
専門の手順を必要なときだけ読む
C7 スクリプト
scripts/<名前>.py
bash で実行したとき
決まった処理を確実にこなす
C8 プラグイン
plugin/<名前>.ts
ツール呼び出しのたび
モデルが飛ばせない処理を差し込む
この表で大事なのは「いつ効くか」の列です。常時読み込まれる部品に文章を足すと、毎回の実行でモデルの文脈(コンテキスト)を圧迫します。リポジトリの編集役向けの指示(harness2/core/prompts.py)には、新しい手順はまず スキル、決まった処理は スクリプト、入力が大きく答えが小さい調べものは サブエージェント にする、という優先順位まで書かれています。
Claude Code を使っている方なら、CLAUDE.md・スキル・フック・サブエージェントを思い浮かべると近いイメージです。Harness² は、そうした部品を AI が自分で書き足したり書き換えたりする仕組みだと考えてください。
Harness² の全体像:2段の「再帰」
Harness² の名前の「²(2乗)」は、改善の輪が2段になっていることを表しています。
1つめの輪は タスク内の再帰(task-level recursion) です。今取り組んでいる仕事のために、ハーネスを試して直します。2つめの輪は ハーネス改善の再帰(harnessing-level recursion) です。直した経験を記録し、次の仕事で「どう直すか」を良くします。
モデルの重みも、各段階への指示文も固定です。変わるのは「この仕事のハーネス」と「編集役が読むライブラリ」の2つだけです。
ここで押さえておきたいのは、次の仕事のハーネスは毎回まっさらな初期状態(H0)から始まる という点です。前の仕事で直したハーネスをそのまま引き継ぐわけではありません。
論文は、前の仕事に合わせ込んだハーネスをそのまま使い回すと、仕事の種類が変わったときに逆効果になりやすいと指摘しています。そこで Harness² は、ハーネスそのものではなく「直し方の経験」を引き継ぎます。編集役が新しい仕事のハーネスを直すとき、ライブラリにある過去のハーネスと経験メモを参考として読み、使える部品だけを選んで取り込む、という形です。
登場する役割を整理しておきます。
役割
論文での呼び名
やること
実行役
Agent Executor
固定のモデルが、ハーネスを使って実際に仕事をこなす(OpenCode か Codex の上で動く)
編集役
Harness Editor
実行の記録を読み、ハーネスの直し案を作る
合成役
Harness Composer
直し案とその実行結果を比べ、最終的なハーネスを作る
選択役
Selector
k>1 のとき、作り手を隠した成果物の中から1つ選ぶ
振り返り役
Experience Reflector
改善の記録全体を読み、ライブラリに経験メモを書く
実行役以外の4つは、どれも同じ「改善用のモデル」が担当します。論文の主な実験では Claude Opus 4.8 が OpenCode 上でこの役を務めました。リポジトリでは HARNESS2_IMPROVER_MODEL で指定でき、実行役と同じモデルにすることもできます。
正解なしで直す仕組み:Propose → Probe → Compose
Harness² の中心は、1回の改善ステップ「Propose(提案)→ Probe(試行)→ Compose(合成)」です。正解がない状況で、どうやって「この変更は効いた」と判断するのかを見ていきましょう。
k=1 の場合、1つの仕事につき実行は4回、編集役のセッションは3回です。採点基準や点数は、どの段階でも編集役に見せません。
1. 基準になる実行をする
まず、今のハーネスのまま1回仕事をさせます(base1)。この実行で残るのは、エージェントが何をしたかの記録(論文では「軌跡」、trajectory)と成果物です。
ここで重要なのは、この実行が 正しかったかどうかは分からない という点です。論文も「基準の実行はハーネスがどう振る舞うかを示すだけで、成果物が正しいかは示さない」と書いています。
2. 2種類の直し案を作る(Propose)
編集役は、基準の実行を読んで直し案を2つ作ります。担当範囲をあえて分けているのが工夫です。
INV 案(invariant、分野を問わないやり方):計画の立て方、ツールの使い方、出力の確認漏れなど、どの分野でも起きる弱点を直します。プロンプトでは「専門知識には触れるな」と指示されます
DOM 案(domain、分野の知識):その職種の人なら知っている手順、成果物の型、確認項目を、スキルなどの形で書きます。プロンプトでは「コマンドやリトライの話はするな」と指示されます
どちらの案も、ハーネスのファイルを書き換えた「すぐ実行できるハーネス一式」として出てきます。
3. 同じ条件で試す(Probe)
2つの案を、同じ仕事・同じモデル・同じ環境 でもう一度ずつ実行します。こうすると、基準の実行と見比べられます。
たとえば「基準の実行では出力ファイルを書いたあと開き直していなかったが、INV 案の実行では検証スクリプトで開き直し、欠けていた節に気づいて書き足した」という違いが見えれば、それがその変更を残す根拠になります。論文はこれを 行動の対比(behavioral contrast) と呼んでいます。
4. 証拠のある変更だけを組み合わせる(Compose)
最後に合成役が、基準の実行、2つの案とその差分、それぞれの実行記録をすべて読み、最終的なハーネスを作ります。選べるのは次の3つです。
両案から効いた変更を 合成 する(INV と DOM がそれぞれ根拠を持つときに期待される形)
1つの案を そのまま採用 する
KEEP_BASE:何も直さない
リポジトリのプロンプトには「KEEP_BASE は失敗ではなく正当な答え。証拠のない変更は退行(悪化)だ」とはっきり書かれています。さらに、判断のたびに「どの実行のどの場面が根拠か」を1行ずつ書くよう求めます。点数が上がりそうだから、ではなく、観察した事実に結びつけて判断させるわけです。
合成したハーネスでもう一度実行し、その成果物を提出します。k=1 ではこの成果物がそのまま採用されます。
「正解なし」でも成り立つ理由と限界
この方式は、成果物の正しさを判定するのではなく、ハーネスの変更が行動をどう変えたか を見ています。「確認スクリプトを入れたら、実際に開き直すようになった」は正解がなくても観察できます。
一方で論文は、この証拠には限界があるとも明言しています。実行には毎回ゆらぎがあり、編集役と実行役が同じ思い込みを持っていれば気づけません。合成役への指示にも「1回の実行で良かった・悪かったは弱い証拠」「基準の実行で繰り返し起きた失敗こそ強い証拠」と書かれています。
k を増やす:並列と逐次
1回の改善ステップの「試す回数」を増やす方法が2つ用意されています。リポジトリでは --k と --mode で切り替えます。
どちらのモードも、1つの仕事あたり実行 4k 回、編集役のセッション 3k 回です。k>1 のときは最後に成果物を1つ選ぶ段階が加わります。
並列(parallel):基準の実行を k 回とり、INV 案と DOM 案を k 個ずつ作って試します。その全部を材料に、k 人の合成役が互いを見ずに合成します。横に広く探る方法です
逐次(sequential):1周目で合成したハーネスと証拠を持ち越して、2周目の Propose → Probe → Compose を行います。縦に深く磨く方法です
k>1 のときは、最後に選択役が k 個の最終候補(並列なら agg1〜aggk、逐次なら c1〜ck)の成果物を比べて1つ選びます。このとき選択役には、どの成果物がどのハーネスから出たかを伏せ、番号もばらばらに並べます。プロンプトには「どの候補も特別扱いしない。手の込んだものを選んでも加点はない」と書かれています。見た目が豪華な成果物に引っ張られる偏りを避けるためです。
論文の WorkBuddy-Bench(Code と Office)の実験では、k を 1 から 5 に増やすと両モードとも単調に点が上がりました。k=5 では並列が逐次を上回り、Code は 76.5→80.8、Office は 79.5→83.4 になっています(並列)。ただし k を増やすほど実行回数とモデルの利用料も比例して増える点には注意が必要です。
ライブラリ:直し方の経験を次の仕事へ
仕事を1つ終えるたびに、振り返り役が改善の記録全体(基準の実行、各案、合成の判断、最終の成果物)を読み、分野ごとのライブラリに書き込みます。中身を、付属デモを実行して実際に書き出されたファイルで見てみましょう。
経験メモには「各変更が何をして、実行がどう変わったか」と「次に持ち越す教訓(最大1つ)」が残ります。根拠が弱ければ教訓は書きません。
ライブラリの1件は、次の2つで構成されます。
採用したハーネス一式:要約した文章ではなく、スキルやスクリプトのファイルそのものを残します
EXPERIENCE.md:なぜこのハーネスが選ばれたか、各変更の判定、次に持ち越す教訓
各変更の判定は helped(効いた)、hurt(悪化させた)、no observed effect(効果なし)、confounded(切り分けられない)の4種類です。振り返り役への指示には「複数の部品を同時に変えて、どれが効いたか切り分けられないときは confounded を選べ」「でっち上げた因果は、後のすべての仕事のライブラリを汚す」とあります。確かな対比が見つからなければ、教訓の代わりに NO_ENTRY と1行だけ書きます。
次の仕事では、このライブラリが編集役の作業フォルダに context/library/ として置かれます。編集役は目次(INDEX.md)を見て関係しそうなものを開き、使いたい部品があれば自分のハーネスへコピーします。そのとき、前の仕事で分かった事実(ファイル同士の対応表など)は「この仕事では仮説にすぎない」ので、目の前の証拠で確かめ直すよう指示されています。
論文の結果を読む
論文は、3つの業務系ベンチマークで評価しています。
ベンチマーク
内容
指標
WorkBuddy-Bench(Tencent)
事務・Web・コードの成果物づくり(Code/Web/Office の3分野)
採点役のスコア(3分野の平均)
JobBench
職種ごとの実務課題。長い実行が必要
採点基準(ルーブリック)による重み付きスコア
LAB(Harvey)
法律業務
採点項目ごとの合格率(Partial)
実行役は Gemini 3.5 Flash と GLM-5 の2種類、土台のハーネスは OpenCode と Codex の2種類で、組み合わせは4通りです。それぞれ3回ずつ、仕事の順番を入れ替えて実行した平均が報告されています。
OpenCode 上の結果です。1回の改善(k=1)で全項目が上がり、並列の再帰(k=5)でさらに上がっています。
主な結果を、私なりに整理すると次のとおりです。
1回の改善だけで、4通りの組み合わせすべて・3ベンチマークすべてで元のハーネスを上回りました。最大の伸びは WorkBuddy-Bench が +11.5(GLM-5、OpenCode)、JobBench が +4.5(Gemini 3.5 Flash、OpenCode)、LAB が +11.8 ポイント(GLM-5、OpenCode)です
弱いモデルほど伸びが大きい傾向があります。WorkBuddy-Bench では、改善後の GLM-5(75.7)が元のハーネスの Gemini 3.5 Flash(72.4)を上回りました
並列の再帰(k=5)を足すと、さらに WorkBuddy-Bench で +2.9〜3.9、JobBench で +3.0〜4.2、LAB で +2.6〜6.7 ポイント上がりました
比較手法との関係では、WorkBuddy-Bench の平均と JobBench のスコアは4通りすべてで Harness² が最高でした。LAB の Gemini 3.5 Flash × Codex だけは、事前に最適化する Meta-Harness が上回っています
何が効いているのか:部品を外す実験
論文は、Harness² の3つの工夫を1つずつ外す実験(アブレーション)も行っています。Gemini 3.5 Flash × OpenCode、k=1 での結果です。
条件
WorkBuddy-Bench
JobBench
Harness²(すべてあり)
77.5
36.0
ライブラリなし
74.4(−3.1)
34.2(−1.8)
INV/DOM の役割分担なし(汎用の編集役2人)
74.6(−2.9)
32.3(−3.7)
合成なし(良さそうな方を1つ採用)
75.9(−1.6)
34.1(−1.9)
どれを外しても点が下がっており、3つとも効いていると読めます。特に JobBench では役割分担を外したときの落ち込みが大きく、「やり方」と「分野の知識」を分けて考えさせる設計の意味が見えます。
試さずに直すと悪化する
もう1つ興味深いのは、実行して確かめずにハーネスを直すと、かえって悪くなる という結果です。WorkBuddy-Bench の Code では、基準の実行も試行もライブラリもなしで直接ハーネスを書き換えると、73.4 から 63.2 に下がりました。Web も 69.9 から 65.0 に下がっています。
一方、k=5 で育てたライブラリだけを持たせると、追加の試行なしでも Code・Web・Office がそれぞれ +0.9、+4.4、+5.1 上がりました。さらに JobBench で育てたライブラリを WorkBuddy-Bench に持ち込んでも、+0.6、+0.5、+2.9 の改善が見られています。
編集役が強いモデルでなくてもよい
「編集役に強いモデルを使っているから伸びるのでは」という疑問に対しては、実行役と編集役を同じモデルにした実験があります(OpenCode、k=2)。GLM-5 から Claude Opus 5 までの5モデルすべてで、3つのベンチマーク全部が改善しました。GLM-5 自身が編集役を務めた場合でも LAB で +13.7 です。
読むときの注意点
論文の数字を自分の仕事に当てはめる前に、次の点を押さえておくとよいでしょう。
追加の計算が必要:k=1 でも1つの仕事に実行4回・編集役のセッション3回(+振り返り)がかかります。論文の計算量の比較では、Harness² は「同じ計算量なら他の手法より高い点」の境界線(パレート前線)上にありますが、元のハーネスより安くなるわけではありません
採点はモデルによる評価:JobBench と WorkBuddy-Bench の点数は、採点役のモデルが付けたものです
失敗例もある:論文の失敗分析では、選択役が 95 点の成果物ではなく 55 点の成果物を選んだ例や、間違った前提が全候補に広がった例が挙げられています
本番のハーネスとの比較:WorkBuddy-Bench では、元の Claude Code・Codex・OpenCode などを使った中で最も良いものと比べても、OpenCode から始めた Harness²(k=5)が各分野で 4.6〜5.8 上回りました。ただし、これは追加の計算を使った上での比較です
答えを見せない・壊れた案を通さないための仕掛け
正解なしで直す仕組みは、裏を返すと「採点基準が少しでも混ざれば、それに合わせ込む試験対策になってしまう」危うさがあります。リポジトリには、そのための対策がいくつも入っています。
①〜⑤はリポジトリのコードとプロンプトに入っている対策です。⑥は利用者が自分で塞ぐ必要があります。
1つずつ補足します。
採点の情報を渡さない:編集役に渡す作業フォルダに RUBRICS.json などの採点基準や、採点結果の痕跡が入っていたら、受け渡しの前に止めます(harness2/core/context.py)。指示文にも「採点基準は存在しない。採点者が何を求めているか推測するな」と書かれています
証拠はデータ扱い:課題ファイルや候補ハーネスの中に命令文が書かれていても従わないよう指示します。候補に入ったスクリプトは実行せず、読んで判断します。プロンプトインジェクション(データに紛れ込ませた命令で AI を操る攻撃)への備えです
形の検査と1回だけの再挑戦:呼び出し方が書かれていないスキル、コンパイルできないスクリプト、スキルが使う補助ファイルの同梱漏れなどは不合格にします。理由を添えて1回だけやり直させ、それでもだめなら元のハーネスのまま進めます
匿名で選ぶ:k>1 の最終選択では、成果物の作り手を伏せ、順番も入れ替えて比べます。選択役は既存の成果物から選ぶだけで、新しく書き直すことはしません
証拠の弱い教訓は残さない:切り分けられない差は confounded、確かな対比がなければ NO_ENTRY です
そして ⑥ が、利用者が自分で手当てしなければならない穴です。リポジトリの実験ガイドには、実行役のネット接続は制限されていない こと、JobBench は各課題の採点基準を Hugging Face で公開していること、公式の検証中に Codex が指示されてもいないのに採点基準をダウンロードしたことが書かれています。評価に使う前に、実行役のマシンから huggingface.co や github.com への通信を、ファイアウォールや DNS の設定で塞ぐよう勧めています。
Mac で動かしてみる(1):インストールと付属デモ
ここからは、実際に手元の Mac で動かした手順と結果です。公式の README は「Linux、Python 3.12、uv」を前提にしていますが、パッケージの準備と付属デモは macOS でもそのまま動きました。
準備するもの
もの
手元で使った版
用途
Python
3.12.15(Homebrew)
Harness² 本体(3.12〜3.13 に対応)
uv
0.12.22
仮想環境とパッケージの導入
Node.js
22.23.2
ベンチマークの準備で npm を使う(20 以上)
Git
macOS 標準
依存リポジトリの取得
LM Studio
Qwen3.8-27B(4bit)
後半のローカル実験で使用
付属デモだけなら Python と uv があれば十分です。モデルの認証情報も要りません。
手順1:取得してインストールする
リポジトリを取得する
git clone google-research/harness2
cd harness2
Python 3.12 の仮想環境を作って有効化する
uv venv --python 3.12 .venv
source .venv/bin/activate
本体と、成果物の描画に使う追加パッケージ(openpyxl・python-pptx)を入れる
uv pip install -e '.[render]'
pyproject.toml を見ると、本体の必須依存は空(dependencies = [])で、Python の標準ライブラリだけで動く作りになっています。
手順2:付属デモを動かす
2つの小さな会計タスクを、並列・逐次の両方の方式で k=2 で回す
harness2-demo --output runs/demo --k 2
このデモは、モデルを呼ばない「台本どおりの編集役」と、本物の Python 実行を組み合わせて、Harness² の流れ全体(実行 → 直し案 → 試行 → 合成 → 選択 → 振り返り → ライブラリ)を通します。手元の実行結果がこちらです。
手元の実測:2タスクとも完了、3.1 秒。直し案によって scripts/summarize.py の「正の数だけ足す」条件が外れ、出金を含めた正しい合計になりました。
デモの課題は「台帳の金額を符号つきで合計せよ」です。ところが初期ハーネスに入っている scripts/summarize.py には、正の数だけを足すバグが仕込まれています。そのため、基準の実行は台帳1で Total: 105.25 を出しますが、正しくは出金の −20.10 を含めた Total: 85.15 です。
編集役(デモでは台本)がこの条件を外すと、試行の実行で合計が変わります。選択役は元の台帳と照らして正しい合計の成果物を選び、振り返り役は「入金と出金が混ざった台帳では符号を両方含める」という教訓をライブラリに残しました。前の節の図6で見た EXPERIENCE.md は、このとき実際に書き出されたものです。
数字も確かめておきます。1タスクあたり、並列(k=2)が実行8回、逐次(k=2)が基準の実行を共有して追加6回なので、2タスクで28回です。編集役の呼び出しは、1タスクあたり両方式の改善段階が 3k×2=12回、選択が2回、振り返りが2回で16回、2タスクで32回になり、表示と一致します。
2つめのタスク(ledger-2)の編集役の作業フォルダを開くと、context/library/ に1つめのタスク(ledger-1)の記録が入っていました。前の仕事の経験が、次の仕事の材料として渡っていることが確認できます。
リポジトリ付属のテストも通りました。
付属テストを実行する(pytest を入れてから)
uv pip install pytest
python -m pytest -q tests
手元の結果: 5 passed, 2 subtests passed in 13.75s
Mac で動かしてみる(2):ローカルモデルだけで JobBench を回す
付属デモは仕組みの確認には十分ですが、編集役が台本なので「AI が本当にハーネスを直せるのか」は分かりません。そこで、実在のベンチマーク JobBench の課題を、モデルを使う役をすべて手元の LM Studio に向けて 回してみました。
公式の手順は Google Cloud の Vertex AI(Gemini や Claude)を前提にしていますが、ソースを読むと、実行役と編集役は OpenCode の「プロバイダー/モデル名」で指定でき、JobBench の採点役は OpenAI 互換の API を指定できる作りでした。そこで、3つの役をすべて1台の LM Studio に向けました。
手元の構成です。論文の主な実験(編集役は Claude Opus 4.8、実行役は Gemini 3.5 Flash など)とは、モデルの強さがまったく違う点に注意してください。
実験の条件
項目
今回の設定
課題
JobBench の職種「technical_writers」の task1・task2(2問を順番に)
task1 の内容
API の CSV・開発者メモ・サポート問い合わせから、移行ガイド・API リファレンス・トラブル対応ガイド・リリースノートの4文書を書く
task2 の内容
変更履歴・利用統計・問い合わせから、移行ガイド・改訂版 API リファレンス・影響分析の CSV を作る
方式
並列、k=1(1問につき実行4回・編集役3回+振り返り1回)
モデル
実行役・編集役・採点役すべて Qwen3.8-27B(4bit、LM Studio、思考なし。採点役だけ JobBench 既定の思考 low)
ネット
OpenCode の webfetch・websearch は禁止。ただし後述のとおり bash の curl は通る
職種を1つに絞って2問にしたのは、ライブラリの働き(task1 の経験が task2 で使われるか)を見るためです。
手順1:ベンチマークを準備する
リポジトリのフォルダで、仮想環境を有効にした状態で実行する
OpenCode(ピン留め版)・bun・JobBench の採点器と課題データを dependencies/ に入れる
harness2-setup jb
設定の確認(モデルはまだ呼ばない)
HARNESS2_SOLVER_MODEL=lmstudio/qwen/qwen3.8-27b harness2-check --bench jb
harness2-setup jb は macOS でも最後まで通りました。GitHub から OpenCode と JobBench の採点器を取得し、Hugging Face から課題データ(566ファイル)を落とします。手元では dependencies/ が合計 3.2GB ほどになりました(OpenCode が 2.5GB)。
なお、README には cp .env.example .env とありますが、確認したコミットのリポジトリには .env.example が入っていませんでした。設定は環境変数で直接渡します。
手順2:LM Studio を OpenCode につなぐ
LM Studio でモデルを読み込み、サーバーを起動します。
LM Studio のサーバーを起動し、Qwen3.8-27B を2本並列で読み込む
lms server start
lms load qwen/qwen3.8-27b --parallel 2 -y
次に、OpenCode に LM Studio をプロバイダーとして登録する設定ファイルを作ります。Harness² は OpenCode を自前の環境で起動しますが、環境変数 OPENCODE_CONFIG で指定したファイルは読み込まれるので、ここに書きます。
{
"$schema": "opencode.ai",
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio (local)",
"options": { "baseURL": "127.0.0.1", "apiKey": "lm-studio" },
"models": {
"qwen/qwen3.8-27b": {
"name": "Qwen3.8 27B (LM Studio)",
"limit": { "context": 131072, "output": 32768 },
"options": { "reasoningEffort": "none" }
}
}
}
},
"permission": { "webfetch": "deny", "websearch": "deny" }
}
reasoningEffort: none で Qwen の思考を切っています。思考ありだと手元では1ターンが数倍遅くなるためです。permission は、採点基準をネットから読みに行くのを防ぐ目的で入れました(ただし後で書くとおり完全には防げません)。
手順3:Mac とローカルモデル向けに3か所を補う
そのままでは Mac で動かない箇所や、ローカルモデルに合わない箇所が3つありました。
timeout コマンドが無い:JobBench の実行役ランナーは GNU の timeout で OpenCode を起動します。macOS には無いので、No such file or directory: 'timeout' で実行役が1回も動かずに終わります
時間切れが短い:OpenCode の実行役は 2400 秒で打ち切られます。ローカルの 27B モデルでは足りない可能性があります
編集役で思考が有効になる:編集役は OpenCode に --variant high を付けて呼ばれます。手元で試すと、これで設定の reasoningEffort: none が上書きされ、思考が有効になりました
1つめは、timeout と同じ使い方ができる小さな Python スクリプトを作り、PATH の先頭に置いて解決しました(Homebrew の coreutils を入れて timeout という名前で呼べるようにしても構いません)。
#!/usr/bin/env python3
"""macOS 用の簡易 timeout:timeout [--kill-after=Ns] 秒数 コマンド..."""
import os, signal, subprocess, sys
def secs(s):
s = s.strip()
mult = {"s": 1, "m": 60, "h": 3600}.get(s[-1])
return float(s[:-1]) * mult if mult else float(s)
args = sys.argv[1:]
kill_after = None
while args and args[0].startswith("--"):
a = args.pop(0)
if a.startswith("--kill-after="):
kill_after = secs(a.split("=", 1)[1])
duration, cmd = secs(args[0]), args[1:]
p = subprocess.Popen(cmd, start_new_session=True)
try:
sys.exit(p.wait(timeout=duration))
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGTERM)
try:
p.wait(timeout=kill_after or 30)
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL)
sys.exit(124)
2つめと3つめは、リポジトリのコードを書き換えずに済むよう、環境変数を入れてから Harness² の CLI を呼ぶ小さなラッパー run_local.py で差し替えました。
"""Harness² のすべてのモデル役を LM Studio に向けて動かす(Google Cloud 不要)"""
import os
import sys
from pathlib import Path
LAB = Path(file).resolve().parent
MODEL = os.environ.get("LOCAL_MODEL", "qwen/qwen3.8-27b")
os.environ.setdefault("HARNESS2_SOLVER_MODEL", f"lmstudio/{MODEL}") # 実行役
os.environ.setdefault("HARNESS2_IMPROVER_MODEL", f"lmstudio/{MODEL}") # 編集役など
os.environ.setdefault("HARNESS2_JUDGE_MODEL", MODEL) # 採点役
os.environ.setdefault("HARNESS2_JUDGE_BASE_URL", "127.0.0.1")
os.environ.setdefault("HARNESS2_JUDGE_API_KEY", "lm-studio") # 指定すると gcloud を使わない
os.environ.setdefault("OPENCODE_CONFIG", str(LAB / "opencode-lmstudio.json"))
os.environ["PATH"] = f"{LAB / 'bin'}:{os.environ['PATH']}" # bin/timeout を先に見つけさせる
from harness2.benchmarks import jobbench # noqa: E402(設定は環境変数を読んでから)
from harness2.core.spec import resolve_channel_model # noqa: E402
ROLLOUT_S = int(os.environ.get("LOCAL_ROLLOUT_TIMEOUT", "5400"))
IMPROVER_S = int(os.environ.get("LOCAL_IMPROVER_TIMEOUT", "5400"))
実行役の時間切れを延ばし、編集役は --variant を付けずに呼ぶ(思考なしのまま)
jobbench._DEFAULT_TIMEOUT = {"oc": ROLLOUT_S, "cx": ROLLOUT_S}
jobbench._channels = lambda: dict.fromkeys(
("proposer", "selector"),
resolve_channel_model(None, None, effort="", timeout=IMPROVER_S),
)
cmd, argv = sys.argv[1], sys.argv[2:]
if cmd == "sweep":
from harness2.cli.run_sweep import main
elif cmd == "judge":
from harness2.cli.judge import main
elif cmd == "report":
from harness2.cli.report import main
elif cmd == "check":
from harness2.cli.check import main
else:
raise SystemExit(f"unknown command {cmd!r}")
raise SystemExit(main(argv))
opencode-lmstudio.json・run_local.py・bin/timeout を local/ というフォルダにまとめ、リポジトリのフォルダから次のように実行しました。
technical_writers の2問を、並列・k=1 で順番に解かせる(約9時間かかった)
export HARNESS2_RUNS_DIR=runs/jb-local-k1
nohup python local/run_local.py sweep --bench jb --k 1 --mode parallel \
--only-domains technical_writers --max-tasks 2 > jb-sweep.log 2>&1 &
終わったら、採用した成果物(agg1)と元のハーネスの成果物(base1)を採点する
python local/run_local.py judge --bench jb --k 1 --mode parallel
python local/run_local.py judge --bench jb --k 1 --mode parallel --bases
python local/run_local.py report --bench jb --k 1 --mode parallel
長時間の実行なので、nohup で端末から切り離しています。途中で止まっても、同じコマンドをもう一度実行すれば終わった段階は飛ばして再開します。
結果1:かかった時間は2問で約9時間
2問とも最後まで完了し、失敗はありませんでした。各段階の時間は次のとおりです(手元の実測。直し案と試行は2本を並行で実行)。
段階
task1
task2
基準の実行(base1)
27分
33分
直し案づくり(INV・DOM の2セッション並行)
53分
35分
直し案の試行(inv1・dom1 の2本並行)
90分
90分
合成(agg1 の編集)
52分
42分
合成したハーネスでの実行(agg1)
48分
37分
振り返り(ライブラリへの記録)
18分
26分
合計
4時間46分
4時間23分
1回の改善(k=1)でも、元のハーネスで1回解くのに比べて8〜11倍の時間がかかっています。ローカルの 27B モデルで2本並行にすると、試行の段階はとくに遅くなりました。費用は電気代だけですが、時間の面では「手元で気軽に回す」ものではない、というのが実感です。
結果2:AI が見つけた弱点と、書いた直し
一番分かりやすかったのは task1 です。基準の実行(base1)は4つの文書を書き上げましたが、API リファレンスの途中で、サンドボックス環境の URL を api.datasync.io と自分で作ってしまいました。元資料の開発者メモには sandbox.api.datasync.io と書いてあります。base1 は何度か直そうとしたものの、最終版でも「ホストは開発者ポータルで案内」とぼかしたままでした。
INV 役(やり方の担当)は、この場面を軌跡の行番号つきで指摘し、次のガードレールを書きました。合成役はこれを採用し、出力の検証用スキル(verify-outputs)と、その中で使う検証スクリプト(validate_outputs.py、159行、標準ライブラリだけで動く)もあわせて取り込みました。
手元の実測:task1 で採用されたガードレール(英語のまま)。「共通する値は元資料から1回だけ決め、全ファイルにそのまま写し、書き終えたら全ファイルを開き直して照合する」という内容です。最後の行は、task2 の最終ハーネスがこれをそのまま使ったことを示しています。
4文書のうち正しいサンドボックス URL(sandbox.api.datasync.io/v2)が書かれていた数を、手元で数え直しました。
実行
正しい URL がある文書
作った URL が残った文書
base1(元のハーネス)
0 / 4
0 / 4(最終版ではぼかした)
inv1(INV 案)
4 / 4
0 / 4
dom1(DOM 案)
0 / 4
0 / 4(URL ごと省いた)
agg1(合成・採用)
4 / 4
0 / 4
INV 案の決まりを入れた2回の実行だけが、4文書すべてで正しい値をそろえていました。
DOM 役(分野の知識の担当)は、「内部向けと書かれた資料は顧客向けの文書に載せない」「リリースノートは決まった節立てで、該当なしの節も残す」といった技術文書の決まりごとをメモリやガードレールに書きました。ただし、これらは最終的なハーネスには採用されませんでした。振り返り役の記録では、DOM 案は4つの部品を同時に変えていて、どれが効いたのか切り分けられない(confounded)とされています。
結果3:経験は次の仕事に持ち越された
task1 の振り返り役は、ライブラリにこう書きました(要約)。
きっかけ:同じ事実を何度も書く「連動した複数の文書」を作る仕事で、モデルが文書ごとに値を作り直したり、ぼかしたりしがちなとき
教訓:ガードレールに、共通の値を1回だけ決めて写し、最後に開き直して照合する短い決まりを置く(10行程度)。検証スキルは補助で、決まりの代わりにはしない
確度:中(効いた対比は1回の実行だけなので、判定は confounded)
続く task2(ペットストア API の移行ガイド・改訂版リファレンス・影響分析 CSV)では、3つの編集役の作業フォルダすべてに、このライブラリが入っていました。合成役(agg1)は判断メモに「ライブラリの task1 のガードレールを、そのまま同じ文面で採用」と書き、実際に task2 の最終ハーネスのガードレール・システムプロンプト・検証スクリプトは、task1 のものと中身が同じでした。検証スキルの説明だけが、task2 で見つかった弱点(CSV を手で書いて数値でない値が混ざった、使ってはいけない旧表記「API Key」が残った)に合わせて書き足されていました。
前の仕事で得た直し方が、次の仕事で「部品ごと選んで再利用」される。論文の言うハーネス改善の再帰が、手元の小さな実験でもそのとおりに動いたことになります。
合成役が「常時読み込む文章は小さく保つ」という指示を守っていたのも印象的でした。task2 の DOM 案は常時読み込む部品を 4,474 バイトまで膨らませていましたが、合成役はそれを採らず、2,249 バイトに抑えています。
結果4:点数
最後に、JobBench の採点基準(ルーブリック)で、元のハーネスの成果物(base1)と、Harness² が採用した成果物(agg1)を採点しました。採点役も同じ Qwen3.8-27B です。JobBench の点数は、採点項目(配点6〜10)ごとに「細かい基準をすべて満たしたら配点を加える」方式で付きます。
手元の実測です。task1 は大きく上がった一方、task2 は下がりました。
課題
base1(元のまま)
agg1(採用)
差
細かい基準の合格数
task1(66点満点)
38点(57.6%)
54点(81.8%)
+24.2
35/40 → 38/40
task2(60点満点)
22点(36.7%)
12点(20.0%)
−16.7
19/32 → 18/32
平均
47.1%
50.9%
+3.8
—
task1 では、base1 が落とした3項目(dataset.deleted が存在しないイベントであることの説明、レート制限の区分、本番とサンドボックスの URL)を agg1 がすべて取りました。3つめはまさに、INV 役が見つけた URL の問題です。逆に「エンドポイントの版と変更履歴の対応」の1項目だけは agg1 が落としています。
task2 の −16.7 は、配点10の1項目「GET /user/login のレスポンスヘッダーを、型・説明・値の例つきで書いているか」の合否が入れ替わっただけで生じていました。base1 は採点役に「4つのヘッダーすべてに型・説明・例がある」と判定され、agg1 は「型と説明はあるが例の値がない」として、この項目を落としています。細かい基準の数で見ると 19 対 18 でほぼ同じです。逆に agg1 は「よくある移行のつまずき」の項目で基準を1つから3つに増やしていますが、全部は満たせず加点には届きませんでした。
なお、base1 も agg1 も、移行ガイドに curl と Python の変更前後のコード例を書いておらず、配点10の項目を落としています(agg1 にあるコードブロックは URL を示す2つだけでした)。ハーネスを直す側(同じ Qwen)も、この抜けには気づきませんでした。採点役の説明には「コードブロックが1つもない」と事実と少し違う記述もあり、ローカルモデルの採点をうのみにしない方がよいことも分かります。
この結果から言えるのは次の範囲です。
言えること:正解を見ずに、実行の対比から「作った値を書かない」「最後に開き直して照合する」という具体的な直しが生まれ、その直しが効く項目では点が上がった(task1)。その経験は、次の課題に部品ごと受け渡された(task2)
言えないこと:Harness² が手元のモデルで平均的に点を上げるかどうか。各1回の実行で、採点の合否は配点の大きい1項目で大きく振れます。論文も各設定を3回ずつ回して平均しています
論文との違い:編集役が Claude Opus 4.8 ではなく実行役と同じ 27B のローカルモデルで、スキルの読み込みは後述の理由で失敗していました
気づいた注意点
手元の実験では、結果の読み方に関わる問題が3つありました。
1. スキルの読み込みが全部失敗していた(Mac の OpenCode)
実行役がスキルを呼ぶたびに、ripgrep archive did not contain executable というエラーが出ていました。今回の版の OpenCode は、PATH に rg(ripgrep)が無いと GitHub から取得して一時フォルダに展開しますが、その一時フォルダを片付けてから実行ファイルの有無を確かめているため、必ず失敗します。Linux では rg が入っていることが多く、表に出にくい問題だと思われます。
今回の実行では、エージェントはエラーのあと SKILL.md をファイルとして直接読んで手順に従ったり、同じ確認を自分でスクリプトに書いて実行したりしていました。スキルは「呼べなかったが、中身はおおむね使われた」状態で、論文の環境とは条件が違います。次のどちらかで直ります。
方法1:Homebrew で ripgrep を入れる(PATH 上の rg が使われる)
brew install ripgrep
方法2:OpenCode が取得済みのアーカイブから rg を取り出し、OpenCode が探す場所に置く
cd ~/.cache/opencode/bin
tar -xzf ripgrep-14.1.1-aarch64-apple-darwin.tar.gz ripgrep-14.1.1-aarch64-apple-darwin/rg
mv ripgrep-14.1.1-aarch64-apple-darwin/rg ./rg && rmdir ripgrep-14.1.1-aarch64-apple-darwin
方法2で rg を置いたあと、簡単なスキル(SKILL.md に合言葉を書いたもの)を OpenCode に読み込ませると、skill ツールが成功し、合言葉を正しく返しました。これから試す方は、実験の前にこの手当てをしておくことをおすすめします。
2. 採点の時間切れは0点として記録される
JobBench の採点器は、採点項目ごとの問い合わせを8本同時に送り、240 秒で返らないと3回まで再試行し、それでもだめならその項目を不合格(0点)として記録します。LM Studio は同時に2本しか処理しないので、そのままだと待たされた項目が「本当は合格かもしれないのに0点」になります。最初は2件を同時に採点していたため計16本が LM Studio に並び、1項目しか終わらないまま25分たったのを見て止め、同時数を2、待ち時間を1時間に変えてやり直しました(run_local.py の追加分)。
run_local.py に追加:JobBench の採点器に渡す引数を、ローカル向けに書き換える
JUDGE_WORKERS = os.environ.get("LOCAL_JUDGE_WORKERS", "2")
JUDGE_TIMEOUT = os.environ.get("LOCAL_JUDGE_TIMEOUT", "3600")
_run = jobbench.subprocess.run
def _judge_run(cmd, *a, **kw):
if isinstance(cmd, list) and any(str(c).endswith("judge.py") for c in cmd):
cmd = list(cmd)
for flag, value in (("--max-workers", JUDGE_WORKERS), ("--timeout-per-rubric", JUDGE_TIMEOUT)):
if flag in cmd:
cmd[cmd.index(flag) + 1] = value
kw["timeout"] = max(int(kw.get("timeout") or 0), 6 * int(JUDGE_TIMEOUT))
return _run(cmd, *a, **kw)
jobbench.subprocess.run = _judge_run
下の分岐の judge のところも、採点を1件ずつにするよう書き換える
elif cmd == "judge":
from harness2.cli import judge as _judge
_judge._PARALLEL = 1
from harness2.cli.judge import main
途中まで書かれた採点ファイルは、次回そのまま「採点済み」として読まれるので、やり直す前に eval_result/ を消しておく必要があります。
3. ネットの禁止は bash で抜けられる
OpenCode の webfetch と websearch は禁止しましたが、エージェントは bash の curl で外部にアクセスしていました。課題が名前を挙げている参考資料(GitHub の gist にある API 文書の見本、Microsoft のスタイルガイドなど)を取りに行ったもので、すべての軌跡を調べた範囲では、Hugging Face 上の採点基準(RUBRICS.json)や JobBench のリポジトリへのアクセスはありませんでした。それでも、ツールの設定だけでは通信を止められないことは確かです。公式ガイドのとおり、評価に使うならファイアウォールや DNS で通信先を塞ぐ必要があります。
トラブルシューティング
手元で実際に引っかかった点と、ソースコードを読んで分かった注意点を、確かめる順にまとめます。とくに④と⑦は、エラーで止まらずに結果だけがゆがむので気をつけてください。
左がログに出る文言、右が対処です。④⑦はエラーで止まらず、結果だけが変わります。
症状(ログの文言)
原因
対処
Refusing to overwrite nonempty dependencies/…
準備が途中で止まった残り
そのフォルダを別名に移して harness2-setup をやり直す
HARNESS2_SOLVER_MODEL is required …
モデルの指定がない(.env.example は同梱されていない)
lmstudio/qwen/qwen3.8-27b のように「プロバイダー/モデル」で環境変数に書く
No such file or directory: 'timeout'
macOS に GNU の timeout が無い
代わりのスクリプトを PATH の先頭に置く(または coreutils)
TIMEOUT や runner wall-timeout
ローカルモデルが遅く、実行役の上限(OpenCode は 2400 秒)を超えた
上限を延ばす(今回は 5400 秒)
ripgrep archive did not contain executable
OpenCode が ripgrep の自動取得に失敗する
brew install ripgrep、または ~/.cache/opencode/bin/rg に置く
編集役が非常に遅く、LM Studio に思考が流れる
編集役は --variant high 付きで呼ばれる
effort を空にして --variant を付けない
no Vertex access token … judge not run
採点役が gcloud のトークンを取りに行く
HARNESS2_JUDGE_BASE_URL と HARNESS2_JUDGE_API_KEY を設定する
採点項目が時間切れで不合格になる
8本同時・240 秒の待ち時間で、時間切れは0点として記録
同時数を減らし、待ち時間を延ばす。途中の eval_result/ は消してからやり直す
自分の仕事に載せるには:アダプターを書く
Harness² は、ベンチマークごとの違いを アダプター(adapter) という1つのクラスに閉じ込めています。harness2/core/spec.py の BenchmarkAdapter には「これを実装すれば方式全体が動く。ここに方式のロジックは一切ない」と書かれています。自分の業務課題で試したいときに書くのは、このアダプターです。
主なメソッドを表にまとめます。
メソッド
返すもの
書くときの注意
tasks()
課題 ID の一覧
順番を固定する(ライブラリの状態を再現できるように)
domain_of(task_id)
分野の名前
ライブラリはこの単位で分かれる
task_prompt(task_id)
エージェントに渡す依頼文
採点基準や期待する出力は抜いておく
task_files(task_id)
元資料のフォルダ
編集役も context/source/ として読む
execute(task_id, harness_dir, run_id)
1回の実行のフォルダ
harness_dir=None は元のハーネス。同じ run_id なら結果を使い回す
delivered(run_dir)
成果物ができたか
何も出なかった実行を見分ける
trajectory(run_dir)
読める形の実行記録
[ACTION]/[OBSERVATION]/[ERROR] の書式にそろえる
deliverable_text(run_dir)
成果物の文字列
エージェントが書いたものだけから取り出す
base_harness()
初期ハーネスのフォルダ
8つの部品のファイルを置く
validate_tree(tree)
なし(不備なら例外)
例外のメッセージがそのまま編集役への差し戻し理由になる
score(run_dir, task_id)
採点結果
選択が終わった後にしか呼ばれない。失敗時は None(0点と区別する)
付属デモの harness2/demo.py(約160行)が、モデルを使わない最小のアダプターとしてそのまま手本になります。デモでは execute() が Python スクリプトを実行するだけですが、ここを OpenCode や Codex の起動に置き換えれば、自分の課題で同じ流れを回せます。
もう1つ面白い作りがあります。課題の説明文(BenchPrompts)に「benchmark」「evaluation」「grader」「rubric」「score」といった語が入っていると、エラーで止まるようになっているのです。エラーには「測り方ではなく仕事そのものを書け。実務家に必要なことを言え」とあります。アダプターを書く人がうっかり採点の話を指示文に混ぜないための安全装置です。
まとめ
Harness² は、AI エージェントのハーネス(指示文・スキル・スクリプトなどの道具立て)を、正解のない仕事の中で AI 自身に直させる枠組みです。要点を振り返ります。
正解の代わりに「行動の違い」を証拠にする:基準の実行、2つの直し案(やり方の INV と分野の知識の DOM)の実行を見比べ、証拠のある変更だけを合成します
直し方の経験を次へ持ち越す:ハーネスそのものは毎回初期状態から始め、採用したハーネス一式と経験メモをライブラリに残して、次の仕事の編集役に読ませます
論文では一貫して改善:1回の改善で3ベンチマーク・4通りの組み合わせすべてが上がり、k を増やすとさらに伸びました。ただし計算量は増えます
Mac でも動く:付属デモはそのまま動きました。JobBench は、timeout の代わりのスクリプトなど3か所を補えば、LM Studio のローカルモデルだけで最後まで回せました(スキルを正しく読ませるには ripgrep の手当ても必要)
手元でも経験の持ち越しを確認:task1 で見つかった「ファイル間で値をそろえる」決まりが、ライブラリ経由で task2 のハーネスに取り込まれました
点数は上下した:task1 は 57.6% → 81.8%、task2 は 36.7% → 20.0%。各1回・採点役も同じ 27B モデルなので、点数の良し悪しは決められません
一方で、採点基準のネット経由の漏れ、実行のたびのゆらぎ、編集役と実行役が同じ思い込みを持つ危うさなど、使う側が気をつけるべき点もはっきりしています。
次の一歩としては、まず harness2-demo で流れをつかみ、付属デモのアダプター(harness2/demo.py)をまねて、自分の小さな業務課題を1つ載せてみるのがおすすめです。採点の仕組みが自分で用意できる課題(決まった形式のレポート、表の集計など)から始めると、改善の効き目を確かめやすくなります。
参考リソース
GitHub:google-research/harness2(Apache 2.0)
論文:Ruiyao Xu ほか「Harness²: Recursive Agent Harnessing for an Open World」、arXiv 2610.14224(2026年10月9日)arxiv.org
プロジェクトページ:harness2.github.io
リポジトリ内のガイド:docs/SETUP.md(準備)、docs/EXPERIMENTS.md(実験の進め方とネットの制限)、docs/BENCHMARKS.md(各ベンチマーク)
JobBench:Job-Bench/job-bench-eval
OpenCode:anomalyco/opencode(Harness² がピン留めしている版の取得元)
Google の Harness² を Mac で動かしてみた
!Google の Harness² を Mac で動かしてみた
はじめに
Claude Code や Codex のような AI エージェントを仕事で使っていると、同じモデルなのに「指示文を1行足しただけで急にうまくいく」「スキルを1つ作ったら失敗が消えた」という経験をよくします。モデルの性能だけでなく、モデルの周りに置く道具立てが結果を大きく左右するからです。
この道具立てを、最近は ハーネス(harness) と呼びます。システムプロンプト、守るべき決まり、スキル、補助スクリプト、サブエージェントなど、モデルを「仕事ができるエージェント」にするための仕組み全体のことです。
ただ、ハーネスの調整はたいてい人の手仕事です。失敗した実行ログを読み、勘で指示文を書き足し、もう一度試す。しかも新しい種類の仕事には 正解も採点基準もない ので、直した結果が本当に良くなったのか確かめにくいという問題があります。
2026年10月9日に Google Cloud AI Research などのチームが論文を公開した Harness²(ハーネス・スクエア) は、この調整を AI 自身に任せる枠組みです。コードも GitHub の google-research/harness2 で Apache 2.0 として公開されています。
まずは、何が変わるのかを図で見てください。左が人の手で直す従来の流れ、右が Harness² の流れです。
!左は人が実行ログを読み勘で指示文を足す従来の流れ、右はHarness²が今のハーネスで一度実行し、直し案を2つ作って同じ仕事で試し、実行の違いを根拠に合成し、経験メモを次に持ち越す流れ
正解がなくても「直す前と後で実行がどう変わったか」は観察できます。Harness² はこの違いを証拠にして直し、直し方の経験まで次の仕事に持ち越します。
この記事では、Harness² の仕組みを初めての人にも分かるように順を追って説明し、論文の結果を整理したうえで、実際に手元の Mac(M5 Max)で動かした結果 を紹介します。公式の手順は Google Cloud(Vertex AI)の Gemini を前提にしていますが、今回は LM Studio のローカルモデル(Qwen3.8-27B)だけ で、実在のベンチマーク JobBench の課題を回してみました。
先に手元の結果を書いておくと、2問のうち1問目は元のハーネスの 57.6% から 81.8% に上がり、2問目は 36.7% から 20.0% に下がりました。点数の上下より印象的だったのは、AI が1問目で見つけた弱点(文書ごとに URL を勝手に作る)への直し方が、ライブラリを通じて2問目のハーネスにそのまま受け継がれたことです。詳しくは後半で紹介します。
この記事で分かること
- ハーネスの8つの部品と、Harness² がどこを書き換えるのか
- 正解なしで直すための「Propose → Probe → Compose」という1回の改善ステップ
- 経験を次の仕事に持ち越す「ライブラリ」の中身
- 論文の結果と、その読み方の注意点
- Mac で付属デモを動かす手順と、ローカルモデルだけで JobBench を回す方法・つまずきどころ
想定読者
AI エージェント(Claude Code、Codex、OpenCode など)を使ったことがあり、プロンプトやスキルを自分で調整した経験がある方を想定しています。Python の仮想環境を作ってコマンドを実行できれば、手元での再現も追えます。
この記事の確認範囲
- 確認日:2026年10月21〜22日
- リポジトリ:google-research/harness2 のコミット 1cfe254(2026年10月13日「Fix arXiv link」)、パッケージ版 0.1.0
- 論文:arXiv 2610.14224 v1(2026年10月9日投稿)
- 検証環境:MacBook Pro(Apple M5 Max、メモリ 128GB)、macOS 26.6.2、Python 3.12.15、uv 0.12.22、LM Studio(Qwen3.8-27B 4bit)
- 論文の数値は論文の表から引用しています。手元で測った数値は「手元の実測」と明記して区別します
ハーネスとは何か:8つの部品
Harness² の論文は、エージェントを「モデル+ハーネス」と捉えます。モデルの重み(学習済みのパラメータ)は一切変えず、周りのハーネスだけを書き換えて性能を上げる、という立場です。
Harness² が書き換えの対象にする部品は8つあります。どれも1つのファイルとして置かれるので、直した内容は「差分」として残り、あとから人が読んで確かめられます。
!Harness²が書き換える8つの部品。文脈づくりのシステムプロンプト・ガードレール・メモリ・スキル、道具の使い方のツールの手引き・スクリプト、仕事の分担のサブエージェント、ツール呼び出しへの割り込みのプラグイン。中央に固定のモデル
常時読み込まれる部品と、呼んだときだけ働く部品があります。Harness² の編集役は「常時の文章は毎回コストがかかる」と指示されています。
8つの部品と、実行時に効くタイミングを表にまとめます。
| 部品 | ファイル | 効くタイミング | 役割 |
|---|---|---|---|
| C1 システムプロンプト | systemprompt.md | 毎回 | 役割と全体の手順 |
| C2 ガードレール | guardrails.md | 毎回 | どんな仕事でも守る決まり |
| C3 メモリ | LongTermMEMORY.md | 毎回 | その分野の不変の事実 |
| C4 ツールの手引き | tool_descriptions/tool_guidance.md | 毎回 | 既存ツールをいつ・どう使うか |
| C5 サブエージェント | sub_agents/<名前>/AGENT.md | task ツールで呼んだとき | 別の文脈で調べ、結果だけ返す |
| C6 スキル | skills/<名前>/SKILL.md | skill ツールで呼んだとき | 専門の手順を必要なときだけ読む |
| C7 スクリプト | scripts/<名前>.py | bash で実行したとき | 決まった処理を確実にこなす |
| C8 プラグイン | plugin/<名前>.ts | ツール呼び出しのたび | モデルが飛ばせない処理を差し込む |
この表で大事なのは「いつ効くか」の列です。常時読み込まれる部品に文章を足すと、毎回の実行でモデルの文脈(コンテキスト)を圧迫します。リポジトリの編集役向けの指示(harness2/core/prompts.py)には、新しい手順はまず スキル、決まった処理は スクリプト、入力が大きく答えが小さい調べものは サブエージェント にする、という優先順位まで書かれています。
Claude Code を使っている方なら、CLAUDE.md・スキル・フック・サブエージェントを思い浮かべると近いイメージです。Harness² は、そうした部品を AI が自分で書き足したり書き換えたりする仕組みだと考えてください。
Harness² の全体像:2段の「再帰」
Harness² の名前の「²(2乗)」は、改善の輪が2段になっていることを表しています。
1つめの輪は タスク内の再帰(task-level recursion) です。今取り組んでいる仕事のために、ハーネスを試して直します。2つめの輪は ハーネス改善の再帰(harnessing-level recursion) です。直した経験を記録し、次の仕事で「どう直すか」を良くします。
!2段の再帰。上の輪は仕事tに対してPropose・Probe・Composeでハーネスを直し成果物を提出する。下の輪は振り返りで何が効いたかを書き、ライブラリに残す。次の仕事も初期ハーネスH0から始めるが、直すときにライブラリを読む
モデルの重みも、各段階への指示文も固定です。変わるのは「この仕事のハーネス」と「編集役が読むライブラリ」の2つだけです。
ここで押さえておきたいのは、次の仕事のハーネスは毎回まっさらな初期状態(H0)から始まる という点です。前の仕事で直したハーネスをそのまま引き継ぐわけではありません。
論文は、前の仕事に合わせ込んだハーネスをそのまま使い回すと、仕事の種類が変わったときに逆効果になりやすいと指摘しています。そこで Harness² は、ハーネスそのものではなく「直し方の経験」を引き継ぎます。編集役が新しい仕事のハーネスを直すとき、ライブラリにある過去のハーネスと経験メモを参考として読み、使える部品だけを選んで取り込む、という形です。
登場する役割を整理しておきます。
| 役割 | 論文での呼び名 | やること |
|---|---|---|
| 実行役 | Agent Executor | 固定のモデルが、ハーネスを使って実際に仕事をこなす(OpenCode か Codex の上で動く) |
| 編集役 | Harness Editor | 実行の記録を読み、ハーネスの直し案を作る |
| 合成役 | Harness Composer | 直し案とその実行結果を比べ、最終的なハーネスを作る |
| 選択役 | Selector | k>1 のとき、作り手を隠した成果物の中から1つ選ぶ |
| 振り返り役 | Experience Reflector | 改善の記録全体を読み、ライブラリに経験メモを書く |
実行役以外の4つは、どれも同じ「改善用のモデル」が担当します。論文の主な実験では Claude Opus 4.8 が OpenCode 上でこの役を務めました。リポジトリでは HARNESS2_IMPROVER_MODEL で指定でき、実行役と同じモデルにすることもできます。
正解なしで直す仕組み:Propose → Probe → Compose
Harness² の中心は、1回の改善ステップ「Propose(提案)→ Probe(試行)→ Compose(合成)」です。正解がない状況で、どうやって「この変更は効いた」と判断するのかを見ていきましょう。
!1回の改善ステップ。base1を今のハーネスで実行し、INV案とDOM案を作り、それぞれ同じ条件で実行して、base1との行動の違いを確かめ、合成役agg1が合成・1案採用・KEEP_BASEのどれかを選んで実行し提出する
k=1 の場合、1つの仕事につき実行は4回、編集役のセッションは3回です。採点基準や点数は、どの段階でも編集役に見せません。
1. 基準になる実行をする
まず、今のハーネスのまま1回仕事をさせます(base1)。この実行で残るのは、エージェントが何をしたかの記録(論文では「軌跡」、trajectory)と成果物です。
ここで重要なのは、この実行が 正しかったかどうかは分からない という点です。論文も「基準の実行はハーネスがどう振る舞うかを示すだけで、成果物が正しいかは示さない」と書いています。
2. 2種類の直し案を作る(Propose)
編集役は、基準の実行を読んで直し案を2つ作ります。担当範囲をあえて分けているのが工夫です。
- INV 案(invariant、分野を問わないやり方):計画の立て方、ツールの使い方、出力の確認漏れなど、どの分野でも起きる弱点を直します。プロンプトでは「専門知識には触れるな」と指示されます
- DOM 案(domain、分野の知識):その職種の人なら知っている手順、成果物の型、確認項目を、スキルなどの形で書きます。プロンプトでは「コマンドやリトライの話はするな」と指示されます
どちらの案も、ハーネスのファイルを書き換えた「すぐ実行できるハーネス一式」として出てきます。
3. 同じ条件で試す(Probe)
2つの案を、同じ仕事・同じモデル・同じ環境 でもう一度ずつ実行します。こうすると、基準の実行と見比べられます。
たとえば「基準の実行では出力ファイルを書いたあと開き直していなかったが、INV 案の実行では検証スクリプトで開き直し、欠けていた節に気づいて書き足した」という違いが見えれば、それがその変更を残す根拠になります。論文はこれを 行動の対比(behavioral contrast) と呼んでいます。
4. 証拠のある変更だけを組み合わせる(Compose)
最後に合成役が、基準の実行、2つの案とその差分、それぞれの実行記録をすべて読み、最終的なハーネスを作ります。選べるのは次の3つです。
- 両案から効いた変更を 合成 する(INV と DOM がそれぞれ根拠を持つときに期待される形)
- 1つの案を そのまま採用 する
- KEEP_BASE:何も直さない
リポジトリのプロンプトには「KEEP_BASE は失敗ではなく正当な答え。証拠のない変更は退行(悪化)だ」とはっきり書かれています。さらに、判断のたびに「どの実行のどの場面が根拠か」を1行ずつ書くよう求めます。点数が上がりそうだから、ではなく、観察した事実に結びつけて判断させるわけです。
合成したハーネスでもう一度実行し、その成果物を提出します。k=1 ではこの成果物がそのまま採用されます。
「正解なし」でも成り立つ理由と限界
この方式は、成果物の正しさを判定するのではなく、ハーネスの変更が行動をどう変えたか を見ています。「確認スクリプトを入れたら、実際に開き直すようになった」は正解がなくても観察できます。
一方で論文は、この証拠には限界があるとも明言しています。実行には毎回ゆらぎがあり、編集役と実行役が同じ思い込みを持っていれば気づけません。合成役への指示にも「1回の実行で良かった・悪かったは弱い証拠」「基準の実行で繰り返し起きた失敗こそ強い証拠」と書かれています。
k を増やす:並列と逐次
1回の改善ステップの「試す回数」を増やす方法が2つ用意されています。リポジトリでは --k と --mode で切り替えます。
!k=2の例。並列は同じ証拠から候補を4つ作り、合成役2人が独立に合成して最後に1つ選ぶ。逐次は1周目の合成c1と証拠を持ち越して2周目を行い、c1とc2から1つ選ぶ
どちらのモードも、1つの仕事あたり実行 4k 回、編集役のセッション 3k 回です。k>1 のときは最後に成果物を1つ選ぶ段階が加わります。
- 並列(parallel):基準の実行を k 回とり、INV 案と DOM 案を k 個ずつ作って試します。その全部を材料に、k 人の合成役が互いを見ずに合成します。横に広く探る方法です
- 逐次(sequential):1周目で合成したハーネスと証拠を持ち越して、2周目の Propose → Probe → Compose を行います。縦に深く磨く方法です
k>1 のときは、最後に選択役が k 個の最終候補(並列なら agg1〜aggk、逐次なら c1〜ck)の成果物を比べて1つ選びます。このとき選択役には、どの成果物がどのハーネスから出たかを伏せ、番号もばらばらに並べます。プロンプトには「どの候補も特別扱いしない。手の込んだものを選んでも加点はない」と書かれています。見た目が豪華な成果物に引っ張られる偏りを避けるためです。
論文の WorkBuddy-Bench(Code と Office)の実験では、k を 1 から 5 に増やすと両モードとも単調に点が上がりました。k=5 では並列が逐次を上回り、Code は 76.5→80.8、Office は 79.5→83.4 になっています(並列)。ただし k を増やすほど実行回数とモデルの利用料も比例して増える点には注意が必要です。
ライブラリ:直し方の経験を次の仕事へ
仕事を1つ終えるたびに、振り返り役が改善の記録全体(基準の実行、各案、合成の判断、最終の成果物)を読み、分野ごとのライブラリに書き込みます。中身を、付属デモを実行して実際に書き出されたファイルで見てみましょう。
!ライブラリのフォルダ構成と、デモで実際に書かれたEXPERIENCE.mdの中身。各変更が何をしたかの行と、次に持ち越す教訓が1つ。右側は次の仕事での使われ方
経験メモには「各変更が何をして、実行がどう変わったか」と「次に持ち越す教訓(最大1つ)」が残ります。根拠が弱ければ教訓は書きません。
ライブラリの1件は、次の2つで構成されます。
- 採用したハーネス一式:要約した文章ではなく、スキルやスクリプトのファイルそのものを残します
- EXPERIENCE.md:なぜこのハーネスが選ばれたか、各変更の判定、次に持ち越す教訓
各変更の判定は helped(効いた)、hurt(悪化させた)、no observed effect(効果なし)、confounded(切り分けられない)の4種類です。振り返り役への指示には「複数の部品を同時に変えて、どれが効いたか切り分けられないときは confounded を選べ」「でっち上げた因果は、後のすべての仕事のライブラリを汚す」とあります。確かな対比が見つからなければ、教訓の代わりに NO_ENTRY と1行だけ書きます。
次の仕事では、このライブラリが編集役の作業フォルダに context/library/ として置かれます。編集役は目次(INDEX.md)を見て関係しそうなものを開き、使いたい部品があれば自分のハーネスへコピーします。そのとき、前の仕事で分かった事実(ファイル同士の対応表など)は「この仕事では仮説にすぎない」ので、目の前の証拠で確かめ直すよう指示されています。
論文の結果を読む
論文は、3つの業務系ベンチマークで評価しています。
| ベンチマーク | 内容 | 指標 |
|---|---|---|
| WorkBuddy-Bench(Tencent) | 事務・Web・コードの成果物づくり(Code/Web/Office の3分野) | 採点役のスコア(3分野の平均) |
| JobBench | 職種ごとの実務課題。長い実行が必要 | 採点基準(ルーブリック)による重み付きスコア |
| LAB(Harvey) | 法律業務 | 採点項目ごとの合格率(Partial) |
実行役は Gemini 3.5 Flash と GLM-5 の2種類、土台のハーネスは OpenCode と Codex の2種類で、組み合わせは4通りです。それぞれ3回ずつ、仕事の順番を入れ替えて実行した平均が報告されています。
!論文の結果。OpenCodeでの元のハーネス、Harness² k=1、並列の再帰k=5のスコア。Gemini 3.5 FlashはWorkBuddy 72.4→77.5→80.8、JobBench 31.5→36.0→39.0、LAB 79.9→81.9→84.5。GLM-5はWorkBuddy 64.2→75.7→79.6、JobBench 20.6→24.2→28.4、LAB 59.6→71.4→78.1
OpenCode 上の結果です。1回の改善(k=1)で全項目が上がり、並列の再帰(k=5)でさらに上がっています。
主な結果を、私なりに整理すると次のとおりです。
- 1回の改善だけで、4通りの組み合わせすべて・3ベンチマークすべてで元のハーネスを上回りました。最大の伸びは WorkBuddy-Bench が +11.5(GLM-5、OpenCode)、JobBench が +4.5(Gemini 3.5 Flash、OpenCode)、LAB が +11.8 ポイント(GLM-5、OpenCode)です
- 弱いモデルほど伸びが大きい傾向があります。WorkBuddy-Bench では、改善後の GLM-5(75.7)が元のハーネスの Gemini 3.5 Flash(72.4)を上回りました
- 並列の再帰(k=5)を足すと、さらに WorkBuddy-Bench で +2.9〜3.9、JobBench で +3.0〜4.2、LAB で +2.6〜6.7 ポイント上がりました
- 比較手法との関係では、WorkBuddy-Bench の平均と JobBench のスコアは4通りすべてで Harness² が最高でした。LAB の Gemini 3.5 Flash × Codex だけは、事前に最適化する Meta-Harness が上回っています
何が効いているのか:部品を外す実験
論文は、Harness² の3つの工夫を1つずつ外す実験(アブレーション)も行っています。Gemini 3.5 Flash × OpenCode、k=1 での結果です。
| 条件 | WorkBuddy-Bench | JobBench |
|---|---|---|
| Harness²(すべてあり) | 77.5 | 36.0 |
| ライブラリなし | 74.4(−3.1) | 34.2(−1.8) |
| INV/DOM の役割分担なし(汎用の編集役2人) | 74.6(−2.9) | 32.3(−3.7) |
| 合成なし(良さそうな方を1つ採用) | 75.9(−1.6) | 34.1(−1.9) |
どれを外しても点が下がっており、3つとも効いていると読めます。特に JobBench では役割分担を外したときの落ち込みが大きく、「やり方」と「分野の知識」を分けて考えさせる設計の意味が見えます。
試さずに直すと悪化する
もう1つ興味深いのは、実行して確かめずにハーネスを直すと、かえって悪くなる という結果です。WorkBuddy-Bench の Code では、基準の実行も試行もライブラリもなしで直接ハーネスを書き換えると、73.4 から 63.2 に下がりました。Web も 69.9 から 65.0 に下がっています。
一方、k=5 で育てたライブラリだけを持たせると、追加の試行なしでも Code・Web・Office がそれぞれ +0.9、+4.4、+5.1 上がりました。さらに JobBench で育てたライブラリを WorkBuddy-Bench に持ち込んでも、+0.6、+0.5、+2.9 の改善が見られています。
編集役が強いモデルでなくてもよい
「編集役に強いモデルを使っているから伸びるのでは」という疑問に対しては、実行役と編集役を同じモデルにした実験があります(OpenCode、k=2)。GLM-5 から Claude Opus 5 までの5モデルすべてで、3つのベンチマーク全部が改善しました。GLM-5 自身が編集役を務めた場合でも LAB で +13.7 です。
読むときの注意点
論文の数字を自分の仕事に当てはめる前に、次の点を押さえておくとよいでしょう。
- 追加の計算が必要:k=1 でも1つの仕事に実行4回・編集役のセッション3回(+振り返り)がかかります。論文の計算量の比較では、Harness² は「同じ計算量なら他の手法より高い点」の境界線(パレート前線)上にありますが、元のハーネスより安くなるわけではありません
- 採点はモデルによる評価:JobBench と WorkBuddy-Bench の点数は、採点役のモデルが付けたものです
- 失敗例もある:論文の失敗分析では、選択役が 95 点の成果物ではなく 55 点の成果物を選んだ例や、間違った前提が全候補に広がった例が挙げられています
- 本番のハーネスとの比較:WorkBuddy-Bench では、元の Claude Code・Codex・OpenCode などを使った中で最も良いものと比べても、OpenCode から始めた Harness²(k=5)が各分野で 4.6〜5.8 上回りました。ただし、これは追加の計算を使った上での比較です
答えを見せない・壊れた案を通さないための仕掛け
正解なしで直す仕組みは、裏を返すと「採点基準が少しでも混ざれば、それに合わせ込む試験対策になってしまう」危うさがあります。リポジトリには、そのための対策がいくつも入っています。
!編集役に答えを見せず壊れた案を通さないための6つの仕掛け。採点の情報を渡さない、証拠はデータ扱い、形の検査と1回だけの再挑戦、匿名で選ぶ、証拠の弱い教訓は残さない、残る穴としてネット経由の漏れ
①〜⑤はリポジトリのコードとプロンプトに入っている対策です。⑥は利用者が自分で塞ぐ必要があります。
1つずつ補足します。
1. 採点の情報を渡さない:編集役に渡す作業フォルダに RUBRICS.json などの採点基準や、採点結果の痕跡が入っていたら、受け渡しの前に止めます(harness2/core/context.py)。指示文にも「採点基準は存在しない。採点者が何を求めているか推測するな」と書かれています
2. 証拠はデータ扱い:課題ファイルや候補ハーネスの中に命令文が書かれていても従わないよう指示します。候補に入ったスクリプトは実行せず、読んで判断します。プロンプトインジェクション(データに紛れ込ませた命令で AI を操る攻撃)への備えです
3. 形の検査と1回だけの再挑戦:呼び出し方が書かれていないスキル、コンパイルできないスクリプト、スキルが使う補助ファイルの同梱漏れなどは不合格にします。理由を添えて1回だけやり直させ、それでもだめなら元のハーネスのまま進めます
4. 匿名で選ぶ:k>1 の最終選択では、成果物の作り手を伏せ、順番も入れ替えて比べます。選択役は既存の成果物から選ぶだけで、新しく書き直すことはしません
5. 証拠の弱い教訓は残さない:切り分けられない差は confounded、確かな対比がなければ NO_ENTRY です
そして ⑥ が、利用者が自分で手当てしなければならない穴です。リポジトリの実験ガイドには、実行役のネット接続は制限されていない こと、JobBench は各課題の採点基準を Hugging Face で公開していること、公式の検証中に Codex が指示されてもいないのに採点基準をダウンロードしたことが書かれています。評価に使う前に、実行役のマシンから huggingface.co や github.com への通信を、ファイアウォールや DNS の設定で塞ぐよう勧めています。
Mac で動かしてみる(1):インストールと付属デモ
ここからは、実際に手元の Mac で動かした手順と結果です。公式の README は「Linux、Python 3.12、uv」を前提にしていますが、パッケージの準備と付属デモは macOS でもそのまま動きました。
準備するもの
| もの | 手元で使った版 | 用途 |
|---|---|---|
| Python | 3.12.15(Homebrew) | Harness² 本体(3.12〜3.13 に対応) |
| uv | 0.12.22 | 仮想環境とパッケージの導入 |
| Node.js | 22.23.2 | ベンチマークの準備で npm を使う(20 以上) |
| Git | macOS 標準 | 依存リポジトリの取得 |
| LM Studio | Qwen3.8-27B(4bit) | 後半のローカル実験で使用 |
付属デモだけなら Python と uv があれば十分です。モデルの認証情報も要りません。
手順1:取得してインストールする
# リポジトリを取得する
git clone https://github.com/google-research/harness2
cd harness2
# Python 3.12 の仮想環境を作って有効化する
uv venv --python 3.12 .venv
source .venv/bin/activate
# 本体と、成果物の描画に使う追加パッケージ(openpyxl・python-pptx)を入れる
uv pip install -e '.[render]'
pyproject.toml を見ると、本体の必須依存は空(dependencies = [])で、Python の標準ライブラリだけで動く作りになっています。
手順2:付属デモを動かす
# 2つの小さな会計タスクを、並列・逐次の両方の方式で k=2 で回す
harness2-demo --output runs/demo --k 2
このデモは、モデルを呼ばない「台本どおりの編集役」と、本物の Python 実行を組み合わせて、Harness² の流れ全体(実行 → 直し案 → 試行 → 合成 → 選択 → 振り返り → ライブラリ)を通します。手元の実行結果がこちらです。
!ターミナルでharness2-demoを実行した画面。2タスクとも ok、実行時間3.1秒、summarize.py の差分で正の数だけ足す条件が消えたこと、実行28回・編集役の呼び出し32回が表示されている
手元の実測:2タスクとも完了、3.1 秒。直し案によって scripts/summarize.py の「正の数だけ足す」条件が外れ、出金を含めた正しい合計になりました。
デモの課題は「台帳の金額を符号つきで合計せよ」です。ところが初期ハーネスに入っている scripts/summarize.py には、正の数だけを足すバグが仕込まれています。そのため、基準の実行は台帳1で Total: 105.25 を出しますが、正しくは出金の −20.10 を含めた Total: 85.15 です。
編集役(デモでは台本)がこの条件を外すと、試行の実行で合計が変わります。選択役は元の台帳と照らして正しい合計の成果物を選び、振り返り役は「入金と出金が混ざった台帳では符号を両方含める」という教訓をライブラリに残しました。前の節の図6で見た EXPERIENCE.md は、このとき実際に書き出されたものです。
数字も確かめておきます。1タスクあたり、並列(k=2)が実行8回、逐次(k=2)が基準の実行を共有して追加6回なので、2タスクで28回です。編集役の呼び出しは、1タスクあたり両方式の改善段階が 3k×2=12回、選択が2回、振り返りが2回で16回、2タスクで32回になり、表示と一致します。
2つめのタスク(ledger-2)の編集役の作業フォルダを開くと、context/library/ に1つめのタスク(ledger-1)の記録が入っていました。前の仕事の経験が、次の仕事の材料として渡っていることが確認できます。
リポジトリ付属のテストも通りました。
# 付属テストを実行する(pytest を入れてから)
uv pip install pytest
python -m pytest -q tests
# 手元の結果: 5 passed, 2 subtests passed in 13.75s
Mac で動かしてみる(2):ローカルモデルだけで JobBench を回す
付属デモは仕組みの確認には十分ですが、編集役が台本なので「AI が本当にハーネスを直せるのか」は分かりません。そこで、実在のベンチマーク JobBench の課題を、モデルを使う役をすべて手元の LM Studio に向けて 回してみました。
公式の手順は Google Cloud の Vertex AI(Gemini や Claude)を前提にしていますが、ソースを読むと、実行役と編集役は OpenCode の「プロバイダー/モデル名」で指定でき、JobBench の採点役は OpenAI 互換の API を指定できる作りでした。そこで、3つの役をすべて1台の LM Studio に向けました。
!3つの役を1台のLM Studioで動かす構成。実行役と編集役はOpenCode経由、採点役はOpenAI互換APIでLM StudioのQwen3.8-27Bを呼ぶ。つなぎの設定opencode-lmstudio.jsonと、Mac向けの補いとしてtimeoutの代わりのスクリプト、時間切れの延長、--variant highの除去
手元の構成です。論文の主な実験(編集役は Claude Opus 4.8、実行役は Gemini 3.5 Flash など)とは、モデルの強さがまったく違う点に注意してください。
実験の条件
| 項目 | 今回の設定 |
|---|---|
| 課題 | JobBench の職種「technical_writers」の task1・task2(2問を順番に) |
| task1 の内容 | API の CSV・開発者メモ・サポート問い合わせから、移行ガイド・API リファレンス・トラブル対応ガイド・リリースノートの4文書を書く |
| task2 の内容 | 変更履歴・利用統計・問い合わせから、移行ガイド・改訂版 API リファレンス・影響分析の CSV を作る |
| 方式 | 並列、k=1(1問につき実行4回・編集役3回+振り返り1回) |
| モデル | 実行役・編集役・採点役すべて Qwen3.8-27B(4bit、LM Studio、思考なし。採点役だけ JobBench 既定の思考 low) |
| ネット | OpenCode の webfetch・websearch は禁止。ただし後述のとおり bash の curl は通る |
職種を1つに絞って2問にしたのは、ライブラリの働き(task1 の経験が task2 で使われるか)を見るためです。
手順1:ベンチマークを準備する
# リポジトリのフォルダで、仮想環境を有効にした状態で実行する
# OpenCode(ピン留め版)・bun・JobBench の採点器と課題データを dependencies/ に入れる
harness2-setup jb
# 設定の確認(モデルはまだ呼ばない)
HARNESS2_SOLVER_MODEL=lmstudio/qwen/qwen3.8-27b harness2-check --bench jb
harness2-setup jb は macOS でも最後まで通りました。GitHub から OpenCode と JobBench の採点器を取得し、Hugging Face から課題データ(566ファイル)を落とします。手元では dependencies/ が合計 3.2GB ほどになりました(OpenCode が 2.5GB)。
なお、README には cp .env.example .env とありますが、確認したコミットのリポジトリには .env.example が入っていませんでした。設定は環境変数で直接渡します。
手順2:LM Studio を OpenCode につなぐ
LM Studio でモデルを読み込み、サーバーを起動します。
# LM Studio のサーバーを起動し、Qwen3.8-27B を2本並列で読み込む
lms server start
lms load qwen/qwen3.8-27b --parallel 2 -y
次に、OpenCode に LM Studio をプロバイダーとして登録する設定ファイルを作ります。Harness² は OpenCode を自前の環境で起動しますが、環境変数 OPENCODE_CONFIG で指定したファイルは読み込まれるので、ここに書きます。
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"lmstudio": {
"npm": "@ai-sdk/openai-compatible",
"name": "LM Studio (local)",
"options": { "baseURL": "http://127.0.0.1:1234/v1", "apiKey": "lm-studio" },
"models": {
"qwen/qwen3.8-27b": {
"name": "Qwen3.8 27B (LM Studio)",
"limit": { "context": 131072, "output": 32768 },
"options": { "reasoningEffort": "none" }
}
}
}
},
"permission": { "webfetch": "deny", "websearch": "deny" }
}
reasoningEffort: none で Qwen の思考を切っています。思考ありだと手元では1ターンが数倍遅くなるためです。permission は、採点基準をネットから読みに行くのを防ぐ目的で入れました(ただし後で書くとおり完全には防げません)。
手順3:Mac とローカルモデル向けに3か所を補う
そのままでは Mac で動かない箇所や、ローカルモデルに合わない箇所が3つありました。
1. timeout コマンドが無い:JobBench の実行役ランナーは GNU の timeout で OpenCode を起動します。macOS には無いので、No such file or directory: 'timeout' で実行役が1回も動かずに終わります
2. 時間切れが短い:OpenCode の実行役は 2400 秒で打ち切られます。ローカルの 27B モデルでは足りない可能性があります
3. 編集役で思考が有効になる:編集役は OpenCode に --variant high を付けて呼ばれます。手元で試すと、これで設定の reasoningEffort: none が上書きされ、思考が有効になりました
1つめは、timeout と同じ使い方ができる小さな Python スクリプトを作り、PATH の先頭に置いて解決しました(Homebrew の coreutils を入れて timeout という名前で呼べるようにしても構いません)。
#!/usr/bin/env python3
"""macOS 用の簡易 timeout:timeout [--kill-after=Ns] 秒数 コマンド..."""
import os, signal, subprocess, sys
def secs(s):
s = s.strip()
mult = {"s": 1, "m": 60, "h": 3600}.get(s[-1])
return float(s[:-1]) * mult if mult else float(s)
args = sys.argv[1:]
kill_after = None
while args and args[0].startswith("--"):
a = args.pop(0)
if a.startswith("--kill-after="):
kill_after = secs(a.split("=", 1)[1])
duration, cmd = secs(args[0]), args[1:]
p = subprocess.Popen(cmd, start_new_session=True)
try:
sys.exit(p.wait(timeout=duration))
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGTERM)
try:
p.wait(timeout=kill_after or 30)
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL)
sys.exit(124)
2つめと3つめは、リポジトリのコードを書き換えずに済むよう、環境変数を入れてから Harness² の CLI を呼ぶ小さなラッパー run_local.py で差し替えました。
"""Harness² のすべてのモデル役を LM Studio に向けて動かす(Google Cloud 不要)"""
import os
import sys
from pathlib import Path
LAB = Path(__file__).resolve().parent
MODEL = os.environ.get("LOCAL_MODEL", "qwen/qwen3.8-27b")
os.environ.setdefault("HARNESS2_SOLVER_MODEL", f"lmstudio/{MODEL}") # 実行役
os.environ.setdefault("HARNESS2_IMPROVER_MODEL", f"lmstudio/{MODEL}") # 編集役など
os.environ.setdefault("HARNESS2_JUDGE_MODEL", MODEL) # 採点役
os.environ.setdefault("HARNESS2_JUDGE_BASE_URL", "http://127.0.0.1:1234/v1")
os.environ.setdefault("HARNESS2_JUDGE_API_KEY", "lm-studio") # 指定すると gcloud を使わない
os.environ.setdefault("OPENCODE_CONFIG", str(LAB / "opencode-lmstudio.json"))
os.environ["PATH"] = f"{LAB / 'bin'}:{os.environ['PATH']}" # bin/timeout を先に見つけさせる
from harness2.benchmarks import jobbench # noqa: E402(設定は環境変数を読んでから)
from harness2.core.spec import resolve_channel_model # noqa: E402
ROLLOUT_S = int(os.environ.get("LOCAL_ROLLOUT_TIMEOUT", "5400"))
IMPROVER_S = int(os.environ.get("LOCAL_IMPROVER_TIMEOUT", "5400"))
# 実行役の時間切れを延ばし、編集役は --variant を付けずに呼ぶ(思考なしのまま)
jobbench._DEFAULT_TIMEOUT = {"oc": ROLLOUT_S, "cx": ROLLOUT_S}
jobbench._channels = lambda: dict.fromkeys(
("proposer", "selector"),
resolve_channel_model(None, None, effort="", timeout=IMPROVER_S),
)
cmd, argv = sys.argv[1], sys.argv[2:]
if cmd == "sweep":
from harness2.cli.run_sweep import main
elif cmd == "judge":
from harness2.cli.judge import main
elif cmd == "report":
from harness2.cli.report import main
elif cmd == "check":
from harness2.cli.check import main
else:
raise SystemExit(f"unknown command {cmd!r}")
raise SystemExit(main(argv))
opencode-lmstudio.json・run_local.py・bin/timeout を local/ というフォルダにまとめ、リポジトリのフォルダから次のように実行しました。
# technical_writers の2問を、並列・k=1 で順番に解かせる(約9時間かかった)
export HARNESS2_RUNS_DIR=runs/jb-local-k1
nohup python local/run_local.py sweep --bench jb --k 1 --mode parallel \
--only-domains technical_writers --max-tasks 2 > jb-sweep.log 2>&1 &
# 終わったら、採用した成果物(agg1)と元のハーネスの成果物(base1)を採点する
python local/run_local.py judge --bench jb --k 1 --mode parallel
python local/run_local.py judge --bench jb --k 1 --mode parallel --bases
python local/run_local.py report --bench jb --k 1 --mode parallel
長時間の実行なので、nohup で端末から切り離しています。途中で止まっても、同じコマンドをもう一度実行すれば終わった段階は飛ばして再開します。
結果1:かかった時間は2問で約9時間
2問とも最後まで完了し、失敗はありませんでした。各段階の時間は次のとおりです(手元の実測。直し案と試行は2本を並行で実行)。
| 段階 | task1 | task2 |
|---|---|---|
| 基準の実行(base1) | 27分 | 33分 |
| 直し案づくり(INV・DOM の2セッション並行) | 53分 | 35分 |
| 直し案の試行(inv1・dom1 の2本並行) | 90分 | 90分 |
| 合成(agg1 の編集) | 52分 | 42分 |
| 合成したハーネスでの実行(agg1) | 48分 | 37分 |
| 振り返り(ライブラリへの記録) | 18分 | 26分 |
| 合計 | 4時間46分 | 4時間23分 |
1回の改善(k=1)でも、元のハーネスで1回解くのに比べて8〜11倍の時間がかかっています。ローカルの 27B モデルで2本並行にすると、試行の段階はとくに遅くなりました。費用は電気代だけですが、時間の面では「手元で気軽に回す」ものではない、というのが実感です。
結果2:AI が見つけた弱点と、書いた直し
一番分かりやすかったのは task1 です。基準の実行(base1)は4つの文書を書き上げましたが、API リファレンスの途中で、サンドボックス環境の URL を https://api.datasync.io/v2/test と自分で作ってしまいました。元資料の開発者メモには https://sandbox.api.datasync.io/v2 と書いてあります。base1 は何度か直そうとしたものの、最終版でも「ホストは開発者ポータルで案内」とぼかしたままでした。
INV 役(やり方の担当)は、この場面を軌跡の行番号つきで指摘し、次のガードレールを書きました。合成役はこれを採用し、出力の検証用スキル(verify-outputs)と、その中で使う検証スクリプト(validate_outputs.py、159行、標準ライブラリだけで動く)もあわせて取り込みました。
!ターミナルでtask1の最終ハーネスのguardrails.mdを表示した画面。複数の文書に共通する値は、元資料から1回だけ決めて全ファイルにそのまま写し、書き終えたら全ファイルを開き直して照合する、という英語のルール。下にtask2の最終ハーネスのガードレールがライブラリのtask1のものと末尾の改行を除いて一致することを示すTrue
手元の実測:task1 で採用されたガードレール(英語のまま)。「共通する値は元資料から1回だけ決め、全ファイルにそのまま写し、書き終えたら全ファイルを開き直して照合する」という内容です。最後の行は、task2 の最終ハーネスがこれをそのまま使ったことを示しています。
4文書のうち正しいサンドボックス URL(sandbox.api.datasync.io/v2)が書かれていた数を、手元で数え直しました。
| 実行 | 正しい URL がある文書 | 作った URL が残った文書 |
|---|---|---|
| base1(元のハーネス) | 0 / 4 | 0 / 4(最終版ではぼかした) |
| inv1(INV 案) | 4 / 4 | 0 / 4 |
| dom1(DOM 案) | 0 / 4 | 0 / 4(URL ごと省いた) |
| agg1(合成・採用) | 4 / 4 | 0 / 4 |
INV 案の決まりを入れた2回の実行だけが、4文書すべてで正しい値をそろえていました。
DOM 役(分野の知識の担当)は、「内部向けと書かれた資料は顧客向けの文書に載せない」「リリースノートは決まった節立てで、該当なしの節も残す」といった技術文書の決まりごとをメモリやガードレールに書きました。ただし、これらは最終的なハーネスには採用されませんでした。振り返り役の記録では、DOM 案は4つの部品を同時に変えていて、どれが効いたのか切り分けられない(confounded)とされています。
結果3:経験は次の仕事に持ち越された
task1 の振り返り役は、ライブラリにこう書きました(要約)。
- きっかけ:同じ事実を何度も書く「連動した複数の文書」を作る仕事で、モデルが文書ごとに値を作り直したり、ぼかしたりしがちなとき
- 教訓:ガードレールに、共通の値を1回だけ決めて写し、最後に開き直して照合する短い決まりを置く(10行程度)。検証スキルは補助で、決まりの代わりにはしない
- 確度:中(効いた対比は1回の実行だけなので、判定は confounded)
続く task2(ペットストア API の移行ガイド・改訂版リファレンス・影響分析 CSV)では、3つの編集役の作業フォルダすべてに、このライブラリが入っていました。合成役(agg1)は判断メモに「ライブラリの task1 のガードレールを、そのまま同じ文面で採用」と書き、実際に task2 の最終ハーネスのガードレール・システムプロンプト・検証スクリプトは、task1 のものと中身が同じでした。検証スキルの説明だけが、task2 で見つかった弱点(CSV を手で書いて数値でない値が混ざった、使ってはいけない旧表記「API Key」が残った)に合わせて書き足されていました。
前の仕事で得た直し方が、次の仕事で「部品ごと選んで再利用」される。論文の言うハーネス改善の再帰が、手元の小さな実験でもそのとおりに動いたことになります。
合成役が「常時読み込む文章は小さく保つ」という指示を守っていたのも印象的でした。task2 の DOM 案は常時読み込む部品を 4,474 バイトまで膨らませていましたが、合成役はそれを採らず、2,249 バイトに抑えています。
結果4:点数
最後に、JobBench の採点基準(ルーブリック)で、元のハーネスの成果物(base1)と、Harness² が採用した成果物(agg1)を採点しました。採点役も同じ Qwen3.8-27B です。JobBench の点数は、採点項目(配点6〜10)ごとに「細かい基準をすべて満たしたら配点を加える」方式で付きます。
!手元の実験のまとめ。task1ではbase1が作ったURLで0/4、INV案のinv1が4/4、DOM案のdom1が0/4、合成したagg1が4/4。振り返りの教訓がライブラリに入り、task2の合成役が同じ文面で採用。採点はtask1が38点から54点へ+24.2、task2が22点から12点へ-16.7
手元の実測です。task1 は大きく上がった一方、task2 は下がりました。
| 課題 | base1(元のまま) | agg1(採用) | 差 | 細かい基準の合格数 |
|---|---|---|---|---|
| task1(66点満点) | 38点(57.6%) | 54点(81.8%) | +24.2 | 35/40 → 38/40 |
| task2(60点満点) | 22点(36.7%) | 12点(20.0%) | −16.7 | 19/32 → 18/32 |
| 平均 | 47.1% | 50.9% | +3.8 | — |
task1 では、base1 が落とした3項目(dataset.deleted が存在しないイベントであることの説明、レート制限の区分、本番とサンドボックスの URL)を agg1 がすべて取りました。3つめはまさに、INV 役が見つけた URL の問題です。逆に「エンドポイントの版と変更履歴の対応」の1項目だけは agg1 が落としています。
task2 の −16.7 は、配点10の1項目「GET /user/login のレスポンスヘッダーを、型・説明・値の例つきで書いているか」の合否が入れ替わっただけで生じていました。base1 は採点役に「4つのヘッダーすべてに型・説明・例がある」と判定され、agg1 は「型と説明はあるが例の値がない」として、この項目を落としています。細かい基準の数で見ると 19 対 18 でほぼ同じです。逆に agg1 は「よくある移行のつまずき」の項目で基準を1つから3つに増やしていますが、全部は満たせず加点には届きませんでした。
なお、base1 も agg1 も、移行ガイドに curl と Python の変更前後のコード例を書いておらず、配点10の項目を落としています(agg1 にあるコードブロックは URL を示す2つだけでした)。ハーネスを直す側(同じ Qwen)も、この抜けには気づきませんでした。採点役の説明には「コードブロックが1つもない」と事実と少し違う記述もあり、ローカルモデルの採点をうのみにしない方がよいことも分かります。
この結果から言えるのは次の範囲です。
- 言えること:正解を見ずに、実行の対比から「作った値を書かない」「最後に開き直して照合する」という具体的な直しが生まれ、その直しが効く項目では点が上がった(task1)。その経験は、次の課題に部品ごと受け渡された(task2)
- 言えないこと:Harness² が手元のモデルで平均的に点を上げるかどうか。各1回の実行で、採点の合否は配点の大きい1項目で大きく振れます。論文も各設定を3回ずつ回して平均しています
- 論文との違い:編集役が Claude Opus 4.8 ではなく実行役と同じ 27B のローカルモデルで、スキルの読み込みは後述の理由で失敗していました
気づいた注意点
手元の実験では、結果の読み方に関わる問題が3つありました。
1. スキルの読み込みが全部失敗していた(Mac の OpenCode)
実行役がスキルを呼ぶたびに、ripgrep archive did not contain executable というエラーが出ていました。今回の版の OpenCode は、PATH に rg(ripgrep)が無いと GitHub から取得して一時フォルダに展開しますが、その一時フォルダを片付けてから実行ファイルの有無を確かめているため、必ず失敗します。Linux では rg が入っていることが多く、表に出にくい問題だと思われます。
今回の実行では、エージェントはエラーのあと SKILL.md をファイルとして直接読んで手順に従ったり、同じ確認を自分でスクリプトに書いて実行したりしていました。スキルは「呼べなかったが、中身はおおむね使われた」状態で、論文の環境とは条件が違います。次のどちらかで直ります。
# 方法1:Homebrew で ripgrep を入れる(PATH 上の rg が使われる)
brew install ripgrep
# 方法2:OpenCode が取得済みのアーカイブから rg を取り出し、OpenCode が探す場所に置く
cd ~/.cache/opencode/bin
tar -xzf ripgrep-14.1.1-aarch64-apple-darwin.tar.gz ripgrep-14.1.1-aarch64-apple-darwin/rg
mv ripgrep-14.1.1-aarch64-apple-darwin/rg ./rg && rmdir ripgrep-14.1.1-aarch64-apple-darwin
方法2で rg を置いたあと、簡単なスキル(SKILL.md に合言葉を書いたもの)を OpenCode に読み込ませると、skill ツールが成功し、合言葉を正しく返しました。これから試す方は、実験の前にこの手当てをしておくことをおすすめします。
2. 採点の時間切れは0点として記録される
JobBench の採点器は、採点項目ごとの問い合わせを8本同時に送り、240 秒で返らないと3回まで再試行し、それでもだめならその項目を不合格(0点)として記録します。LM Studio は同時に2本しか処理しないので、そのままだと待たされた項目が「本当は合格かもしれないのに0点」になります。最初は2件を同時に採点していたため計16本が LM Studio に並び、1項目しか終わらないまま25分たったのを見て止め、同時数を2、待ち時間を1時間に変えてやり直しました(run_local.py の追加分)。
# run_local.py に追加:JobBench の採点器に渡す引数を、ローカル向けに書き換える
JUDGE_WORKERS = os.environ.get("LOCAL_JUDGE_WORKERS", "2")
JUDGE_TIMEOUT = os.environ.get("LOCAL_JUDGE_TIMEOUT", "3600")
_run = jobbench.subprocess.run
def _judge_run(cmd, *a, **kw):
if isinstance(cmd, list) and any(str(c).endswith("judge.py") for c in cmd):
cmd = list(cmd)
for flag, value in (("--max-workers", JUDGE_WORKERS), ("--timeout-per-rubric", JUDGE_TIMEOUT)):
if flag in cmd:
cmd[cmd.index(flag) + 1] = value
kw["timeout"] = max(int(kw.get("timeout") or 0), 6 * int(JUDGE_TIMEOUT))
return _run(cmd, *a, **kw)
jobbench.subprocess.run = _judge_run
# 下の分岐の judge のところも、採点を1件ずつにするよう書き換える
# elif cmd == "judge":
# from harness2.cli import judge as _judge
# _judge._PARALLEL = 1
# from harness2.cli.judge import main
途中まで書かれた採点ファイルは、次回そのまま「採点済み」として読まれるので、やり直す前に eval_result/ を消しておく必要があります。
3. ネットの禁止は bash で抜けられる
OpenCode の webfetch と websearch は禁止しましたが、エージェントは bash の curl で外部にアクセスしていました。課題が名前を挙げている参考資料(GitHub の gist にある API 文書の見本、Microsoft のスタイルガイドなど)を取りに行ったもので、すべての軌跡を調べた範囲では、Hugging Face 上の採点基準(RUBRICS.json)や JobBench のリポジトリへのアクセスはありませんでした。それでも、ツールの設定だけでは通信を止められないことは確かです。公式ガイドのとおり、評価に使うならファイアウォールや DNS で通信先を塞ぐ必要があります。
トラブルシューティング
手元で実際に引っかかった点と、ソースコードを読んで分かった注意点を、確かめる順にまとめます。とくに④と⑦は、エラーで止まらずに結果だけがゆがむので気をつけてください。
!動かないときの確認順。準備、設定、実行役のtimeout、スキルのripgrep、編集役の思考、採点のトークン、採点の時間切れの7項目と、それぞれの対処
左がログに出る文言、右が対処です。④⑦はエラーで止まらず、結果だけが変わります。
| 症状(ログの文言) | 原因 | 対処 |
|---|---|---|
| Refusing to overwrite nonempty dependencies/… | 準備が途中で止まった残り | そのフォルダを別名に移して harness2-setup をやり直す |
| HARNESS2_SOLVER_MODEL is required … | モデルの指定がない(.env.example は同梱されていない) | lmstudio/qwen/qwen3.8-27b のように「プロバイダー/モデル」で環境変数に書く |
| No such file or directory: 'timeout' | macOS に GNU の timeout が無い | 代わりのスクリプトを PATH の先頭に置く(または coreutils) |
| TIMEOUT や runner wall-timeout | ローカルモデルが遅く、実行役の上限(OpenCode は 2400 秒)を超えた | 上限を延ばす(今回は 5400 秒) |
| ripgrep archive did not contain executable | OpenCode が ripgrep の自動取得に失敗する | brew install ripgrep、または ~/.cache/opencode/bin/rg に置く |
| 編集役が非常に遅く、LM Studio に思考が流れる | 編集役は --variant high 付きで呼ばれる | effort を空にして --variant を付けない |
| no Vertex access token … judge not run | 採点役が gcloud のトークンを取りに行く | HARNESS2_JUDGE_BASE_URL と HARNESS2_JUDGE_API_KEY を設定する |
| 採点項目が時間切れで不合格になる | 8本同時・240 秒の待ち時間で、時間切れは0点として記録 | 同時数を減らし、待ち時間を延ばす。途中の eval_result/ は消してからやり直す |
自分の仕事に載せるには:アダプターを書く
Harness² は、ベンチマークごとの違いを アダプター(adapter) という1つのクラスに閉じ込めています。harness2/core/spec.py の BenchmarkAdapter には「これを実装すれば方式全体が動く。ここに方式のロジックは一切ない」と書かれています。自分の業務課題で試したいときに書くのは、このアダプターです。
主なメソッドを表にまとめます。
| メソッド | 返すもの | 書くときの注意 |
|---|---|---|
| tasks() | 課題 ID の一覧 | 順番を固定する(ライブラリの状態を再現できるように) |
| domain_of(task_id) | 分野の名前 | ライブラリはこの単位で分かれる |
| task_prompt(task_id) | エージェントに渡す依頼文 | 採点基準や期待する出力は抜いておく |
| task_files(task_id) | 元資料のフォルダ | 編集役も context/source/ として読む |
| execute(task_id, harness_dir, run_id) | 1回の実行のフォルダ | harness_dir=None は元のハーネス。同じ run_id なら結果を使い回す |
| delivered(run_dir) | 成果物ができたか | 何も出なかった実行を見分ける |
| trajectory(run_dir) | 読める形の実行記録 | [ACTION]/[OBSERVATION]/[ERROR] の書式にそろえる |
| deliverable_text(run_dir) | 成果物の文字列 | エージェントが書いたものだけから取り出す |
| base_harness() | 初期ハーネスのフォルダ | 8つの部品のファイルを置く |
| validate_tree(tree) | なし(不備なら例外) | 例外のメッセージがそのまま編集役への差し戻し理由になる |
| score(run_dir, task_id) | 採点結果 | 選択が終わった後にしか呼ばれない。失敗時は None(0点と区別する) |
付属デモの harness2/demo.py(約160行)が、モデルを使わない最小のアダプターとしてそのまま手本になります。デモでは execute() が Python スクリプトを実行するだけですが、ここを OpenCode や Codex の起動に置き換えれば、自分の課題で同じ流れを回せます。
もう1つ面白い作りがあります。課題の説明文(BenchPrompts)に「benchmark」「evaluation」「grader」「rubric」「score」といった語が入っていると、エラーで止まるようになっているのです。エラーには「測り方ではなく仕事そのものを書け。実務家に必要なことを言え」とあります。アダプターを書く人がうっかり採点の話を指示文に混ぜないための安全装置です。
まとめ
Harness² は、AI エージェントのハーネス(指示文・スキル・スクリプトなどの道具立て)を、正解のない仕事の中で AI 自身に直させる枠組みです。要点を振り返ります。
- 正解の代わりに「行動の違い」を証拠にする:基準の実行、2つの直し案(やり方の INV と分野の知識の DOM)の実行を見比べ、証拠のある変更だけを合成します
- 直し方の経験を次へ持ち越す:ハーネスそのものは毎回初期状態から始め、採用したハーネス一式と経験メモをライブラリに残して、次の仕事の編集役に読ませます
- 論文では一貫して改善:1回の改善で3ベンチマーク・4通りの組み合わせすべてが上がり、k を増やすとさらに伸びました。ただし計算量は増えます
- Mac でも動く:付属デモはそのまま動きました。JobBench は、timeout の代わりのスクリプトなど3か所を補えば、LM Studio のローカルモデルだけで最後まで回せました(スキルを正しく読ませるには ripgrep の手当ても必要)
- 手元でも経験の持ち越しを確認:task1 で見つかった「ファイル間で値をそろえる」決まりが、ライブラリ経由で task2 のハーネスに取り込まれました
- 点数は上下した:task1 は 57.6% → 81.8%、task2 は 36.7% → 20.0%。各1回・採点役も同じ 27B モデルなので、点数の良し悪しは決められません
一方で、採点基準のネット経由の漏れ、実行のたびのゆらぎ、編集役と実行役が同じ思い込みを持つ危うさなど、使う側が気をつけるべき点もはっきりしています。
次の一歩としては、まず harness2-demo で流れをつかみ、付属デモのアダプター(harness2/demo.py)をまねて、自分の小さな業務課題を1つ載せてみるのがおすすめです。採点の仕組みが自分で用意できる課題(決まった形式のレポート、表の集計など)から始めると、改善の効き目を確かめやすくなります。
参考リソース
- GitHub:google-research/harness2(Apache 2.0)
- 論文:Ruiyao Xu ほか「Harness²: Recursive Agent Harnessing for an Open World」、arXiv 2610.14224(2026年10月9日)arxiv.org
- プロジェクトページ:harness2.github.io
- リポジトリ内のガイド:docs/SETUP.md(準備)、docs/EXPERIMENTS.md(実験の進め方とネットの制限)、docs/BENCHMARKS.md(各ベンチマーク)
- JobBench:Job-Bench/job-bench-eval
- OpenCode:anomalyco/opencode(Harness² がピン留めしている版の取得元)
1 like
Mentions
Harness : quand l'agent améliore lui-même le harness qui l'entoure Quand un agent déçoit, le premier réflexe est de changer de modèle. Pourtant, un même modèle peut passer de décevant à convaincant selon son prompt système, les outils qu’on lui décrit ou sa façon de relire son travail avant de rendre un livrable. Cet environnement a un nom : le harness. Google Research vient de publier en open source Harness², un framework qui le fait évoluer automatiquement, sans toucher aux poids du modèle. Le papier et le site du projet détaillent la méthode, les chiffres et les limites. > TL;DR > > - Harness² optimise le harness d’un agent (prompt, mémoire, outils, sous-agents…) au lieu du modèle. > - Deux boucles : une par tâche (Propose, Probe, Compose), une qui apprend de tâche en tâche. > - Selon Google, une seule étape suffit à améliorer GLM-5 et Gemini 3.5 Flash sur trois benchmarks. Le coût en calcul et la vérification sans réponse de référence restent les points à surveiller. Nous verrons ce qu’est un harness, comment fonctionnent les deux niveaux de récursion, puis nous lancerons la démo locale avant de regarder les limites. Le harness : tout ce qui entoure le modèle Un modèle seul ne fait que produire du texte. Ce qui en fait un agent, c’est le système autour : prompts, outils, mémoire, orchestration. Harness² en expose huit composants modifiables : - Construction du contexte : prompt système, garde-fous, mémoire, skills - Interface d’outils : consignes d’utilisation des outils, scripts - Orchestration : sous-agents - Middleware : plugins !Les huit composants du harness regroupés par rôle à l’exécution : contexte, outils, délégation et middleware L’optimisation de ce système s’appelle le _harnessing_. Les équipes qui maintiennent un AGENTS.md ou des skills à la main font déjà cette boucle : observer un échec, réécrire une consigne, relancer. Harness² la confie à des agents. Premier niveau : Propose, Probe, Compose Pour la tâche en cours, le framework enchaîne trois étapes : 1. Propose : un éditeur analyse une première exécution et propose des modifications du harness. Il en existe deux familles : des règles générales de comportement (par exemple relire un fichier après l’avoir écrit) et des conseils propres au domaine de la tâche. 2. Probe : chaque variante est exécutée sur la même tâche. 3. Compose : un composeur compare les trajectoires obtenues (appels d’outils, observations, fichiers produits) et assemble un harness final à partir des changements dont l’effet est observable. Aucune réponse de référence n’est utilisée : le composeur s’appuie uniquement sur la comparaison des comportements entre exécutions. !Pipeline de Harness² : l’exécuteur lance le harness initial, l’éditeur propose des modifications, les sondes les testent et le composeur produit le harness final Second niveau : apprendre à améliorer Sans mémoire, chaque tâche repartirait de zéro. Après chaque tâche, Harness² conserve donc deux choses dans un _improvement harness_ persistant : le harness qui a été adopté, et les leçons que les traces d’exécution permettent d’appuyer (par exemple, relire le fichier final avant de le rendre). Ces leçons guident les propositions faites sur les tâches suivantes. En revanche, une nouvelle tâche ne reprend pas le harness spécialisé de la précédente : elle repart du harness de base. Un réglage pensé pour analyser un contrat pourrait gêner sur une tâche de tableur, donc le framework garde ce qu’il a appris, pas la configuration elle-même. Parallèle ou séquentiel Le budget de récursion se règle avec --k, de deux façons : | Mode | Comportement | | --- | --- | | --mode parallel | Teste K candidats généraux et K candidats de domaine, puis produit K compositions | | --mode sequential | Enchaîne K étapes Propose, Probe, Compose, en reportant le harness affiné et les preuves | Le mode parallèle explore plus large, le séquentiel affine plus en profondeur. Dans les deux modes, dès que K dépasse 1, un sélecteur compare à l’aveugle les livrables obtenus et n’en garde qu’un, sans savoir de quel harness il vient. !Les deux modes de récursion : parallèle avec k candidats testés simultanément, séquentiel avec k étapes successives Essayer en local La démo ne demande aucun identifiant de modèle : elle lance les deux modes avec des éditeurs déterministes et de vraies exécutions Python. Le dépôt indique Linux et Python 3.12 comme prérequis, mais la démo a aussi tourné sur macOS lors de nos essais. # Environnement Python 3.12 avec uv uv venv --python 3.12 .venv source .venv/bin/activate # Installation du projet depuis la racine du dépôt cloné uv pip install -e '.[render]' # Démo locale, budget de récursion k=2 harness2-demo --output runs/demo --k 2 Pour les benchmarks, Harness² ne remplace pas votre agent : il le pilote. Cet agent s’appelle l’exécuteur. C’est OpenCode par défaut, ou Codex (l’agent de code d’OpenAI) avec l’option --substrate cx. Il faut aussi accéder à des modèles via Vertex AI, et installer Node.js 20+ et gcloud. L’exemple du README lance une tâche du benchmark LAB, puis l’évalue et produit le rapport : harness2-setup lab harness2-check --bench lab harness2 --bench lab --k 1 --mode parallel --only-domains antitrust-competition --max-tasks 1 harness2-judge --bench lab --k 1 --mode parallel harness2-report --bench lab --k 1 --mode parallel Ces runs consomment des appels API : le README recommande de commencer par une petite sélection. Attention, --max-tasks plafonne le nombre de tâches par domaine, pas sur l’ensemble du run. Résultats annoncés et limites Google a évalué Harness² sur trois benchmarks : JobBench (travail professionnel), LAB (tâches juridiques) et WorkBuddy-Bench (bureautique, web et code). Selon ses auteurs, une seule étape améliore GLM-5 et Gemini 3.5 Flash par rapport à leur harness de base : jusqu’à +11,5 points sur WorkBuddy-Bench, +4,5 sur JobBench et +11,8 points de pourcentage sur le taux de réussite partielle de LAB. Un budget de récursion plus élevé (un --k plus grand) apporte des gains supplémentaires. Deux détails pèsent dans la lecture de ces résultats, détaillés dans le papier. L’éditeur et le sélecteur sont Claude Opus 4.8, et le juge de JobBench aussi : un biais est possible. Les échantillons sont petits : 34 tâches de test pour JobBench et 100 pour LAB, avec des scores moyennés sur 3 runs. Le calcul d’abord : chaque proposition, chaque sonde et chaque composition est une exécution de plus de l’agent, donc des tokens et du temps. Plus --k est élevé, plus la facture grimpe. Le choix du meilleur candidat ensuite. Aucune bonne réponse n’est connue pendant l’exécution : le composeur s’appuie sur le comportement observé et, dès que --k dépasse 1, le sélecteur final choisit sans connaître la bonne réponse. Il peut se tromper. Les auteurs relèvent eux-mêmes des régressions : sur une tâche de code de WorkBuddy-Bench, le sélecteur a retenu une sortie notée 55 alors qu’une autre atteignait 95. La sécurité enfin. Garde-fous, scripts et plugins font partie du harness, donc l’éditeur automatique peut les modifier. Côté exécution, l’agent a un accès réseau sans restriction, et l’exécuteur Codex tourne sans sandbox. Le README demande de restreindre le réseau avant toute évaluation, car les critères de notation des benchmarks peuvent se trouver en ligne. Reste le statut du projet : le README précise que ce n’est pas un produit officiellement supporté par Google. À traiter comme un projet à expérimenter avant de l’envisager en production. Et ensuite ? Harness² propose de traiter le harness comme un ensemble de fichiers que l’on peut lire et mesurer, plutôt que comme un réglage fait une fois à la main. Son point faible est la vérification : sans bonne réponse connue, rien ne garantit qu’un harness « amélioré » le soit vraiment. Pour tester sans risque, une équipe peut garder un harness de base figé, laisser les révisions se faire dans un environnement isolé, puis comparer la qualité obtenue au calcul dépensé. 🎓 Aller plus loin avec les agents Vous voulez construire votre propre agent IA ? En une journée, la formation Développer son propre agent de TechTown couvre : - la différence entre un agent et un chatbot ; - l’architecture d’un agent : sessions, mémoire, grounding et MCP ; - les tests, les guardrails et le déploiement ; - trois ateliers pour créer un agent avec ADK, lui ajouter des tools et le brancher à un serveur MCP.
Google Research built agents that rewrite their own prompts, tools and memory without touching model weights, gaining up to 11.8 points on pro benchmarks. 🔗 google-research/harness2 Credits: google-research/harness2 #harness2 #python #aiagents
[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](/2026/10/16/adk-graph-workflow-go/): 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](/2026/10/17/adk-graph-workflow-go-2/): 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](/2026/10/20/adk-graph-workflow-go-3/): 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](/2026/10/21/adk-graph-workflow-go-4/): 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](/2026/10/22/adk-graph-workflow-go-5/): 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](/2026/10/24/adk-graph-workflow-go-6/): 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 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 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 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: 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 } 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: 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](/2026/10/26/adk-graph-workflow-go-8/) 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 (8)] When ADK Go Graph Workflow Meets Google Research Harness (Part 2): Integrating Phase 4 Eval Datas !Harness² Part 2: LineCardAdapter Bridging Phase 4 Eval & Phase 5 OpenTelemetry Spans Introduction: Why Decoupling Prompts Isn't Enough? In the previous post [[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Recursive Agent Framework and c1..c8 Decoupling](/2026/10/25/adk-graph-workflow-go-7/) (corresponding to Issue #16), we introduced Google Research's google-research/harness2 (arXiv:2610.14224) and decoupled the prompts, guardrails, long-term memory, and on-demand skills of the LINE AI Business Card Assistant (kkdai/line-card-assistant) into an editable c1..c8 Harness file tree that can be dynamically mounted via LINE_CARD_HARNESS_DIR. However, simply having a hot-reloadable c1..c8 file tree isn't enough; the Harness² dual-layer recursive optimization engine (harness2/core/recursion.py) still cannot directly optimize our Go Agent. This is because Harness² requires a standardized bridge — the BenchmarkAdapter — to answer three questions: 1. list_tasks() (What are the test questions?): What test cases should the Agent challenge? What are the public inputs (Prompt / Image) for each case, and what are the hidden scoring criteria (Rubrics)? 2. run() (How does the Agent think and execute?): When a candidate Harness $H’$ is mounted to execute a specific task, what is the Agent's final output (deliverable)? More importantly, what decision trajectory did it follow across the 13 nodes of the Graph Workflow (trajectory.md)? 3. score() (How was the performance?): Which independent scoring items (per_rubric) did the final output and execution trajectory pass, and where did it lose points? Conveniently, in Phase 4 (PR #13), we already established the eval/datasets/ automated evaluation benchmark, and in Phase 5 (PR #14), we added workflow.node.* OpenTelemetry Spans to all 13 Graph Nodes! In this post (Part 2, corresponding to Issue #17 and PR #19), we will merge these two existing assets into the Harness² LineCardAdapter and explain in depth "Why must it be designed this way?". --- I. Why This Change? Two Core Design Philosophies Before diving into the code, many engineers often ask two questions: 1. _"Why not just feed the entire eval/datasets/*.json file to the optimizer LLM and let it modify the Prompt itself?"_ 2. _"Why do we need to convert Phase 5 OpenTelemetry Spans into trajectory.md? Isn't the final LINE reply text and total score enough?"_ These two questions touch upon the most core engineering designs in the Harness² paper: the Zero-Leakage Barrier and Behavioral Trajectory Contrast. 1. Why the "Zero-Leakage Barrier"? Preventing the Optimizer from "Cheating by Memorizing Answers" Imagine this: if we expose the golden answers in eval/datasets/harness2-challenge-dataset.json (e.g., expected_contains: ["+81-3-1234-5678", "Kenta Sato"]) directly to the Agent or the Harness² Harvester LLM, what happens? The laziest and easiest way for the optimizer to get a perfect score is to write a single line in systemprompt.md: > _"If the input contains Sato, please directly output +81-3-1234-5678 and Kenta Sato."_ In machine learning, this is known as Test Set Leakage / Overfitting to Rubrics. The Agent appears to score 100 on the benchmark, but it crashes immediately when it encounters a new Japanese business card in the real world! flowchart LR Dataset["eval/datasets/*.json\nComplete Test Cases\n(incl. expected_contains / expected_route)"] subgraph Barrier["Zero-Leakage Barrier"] Brief["TaskBrief (Public Task Sheet)\nContains only task_id, prompt, session_input\n❌ Never contains expected_* golden answers"] Scorer["Isolated Scorer (Black-box Grader)\nHolds independent hidden r1..r4 Rubrics"] end Agent["Go ADK Graph Workflow\n(cmd/harness-runner)"] Harvester["Harness² Harvester\n(Propose / Probe / Compose)"] Dataset --> Brief Dataset --> Scorer Brief -->|"Public Input"| Agent Agent -->|"Deliverable + OTel trajectory.md"| Scorer Scorer -->|"Returns only binary scores {r1: 1.0, r2: 0.0}\nand OTel execution trajectory"| Harvester In the Harness² BenchmarkAdapter architecture: - TaskBrief (Public Task Sheet): Can only contain user input (prompt and simulated images/context). It is strictly forbidden from containing any expected_contains, expected_not_contains, or correct answer strings. - score() (Black-box Grader): After comparing answers in an isolated environment, it only returns "which Rubric received a 1.0 or 0.0" (e.g., r3_e164_jp_phone_normalization: 0.0), without leaking the expected string to the optimizer. - As a result, the Harness² optimizer must truly reason out general domain rules (e.g., "When converting Japanese phone numbers +81 (0)3-... to E.164, the domestic trunk prefix (0) must be removed") by reading the Agent's execution trajectory (trajectory.md), rather than hard-coding answers into the Prompt! 2. Why Convert Phase 5 OpenTelemetry Spans into trajectory.md? Opening the Black Box of the 13-Node Graph Our LINE AI Business Card Assistant is not a simple "single LLM call," but an ADK 2.0 Directed Acyclic Graph (Graph Workflow) consisting of 13 nodes: - Business Card Ingestion Pipeline: RouterNode $\rightarrow$ VisionOCRNode $\rightarrow$ Parallel AddFanOut (CleanAndDedupNode, CompanyEnrichNode, ContextMatchNode) $\rightarrow$ AddFanIn (JoinPreviewNode) $\rightarrow$ BuildPreviewNode $\rightarrow$ HITLConfirmNode $\rightarrow$ ApplyEditNode / SaveCardNode. - Smart Search and Icebreaker Pipeline: RouterNode $\rightarrow$ QueryIntentNode $\rightarrow$ HybridSearchNode / BroadSearchNode $\rightarrow$ FormatCarouselNode / EmailDraftNode. Without Phase 5's OpenTelemetry Spans, when a user searches for _"Help me find a Sales Director in Hsinchu Science Park working on AI chips"_ but gets _"No matching business cards found"_ (score 0.25), the optimizer only sees this final failure message and cannot determine: - Did the RouterNode misjudge the search intent as chat? - Did the QueryIntentNode fail to expand the colloquial "Hsinchu Science Park" into the database's address Hsinchu, or fail to expand "Sales Director" into Sales Director? - Was there an error in the SQL query logic of the HybridSearchNode? When we collect and render Phase 5's OpenTelemetry workflow.node.* Spans into a structured trajectory.md, Harness² can clearly see the input, output, duration, routing decisions, and loaded skills of every Graph Node during the Propose and Probe stages! --- II. Implementation Part 2: Building the LineCardAdapter in Three Steps Referring to the architecture diagram at the beginning of this article, we completed the implementation of Issue #17 through three core components: 1. internal/harness/trajectory.go: Mounts an in-memory OpenTelemetry SpanRecorder on the Go side to export the execution details of the 13 Graph Nodes as trajectory.md. 2. cmd/harness-runner/main.go: Provides a deterministic subprocess CLI for the Python Harness² to call, receiving --harness-dir and --task-json, and outputting a JSON containing deliverable, trajectory_markdown, and spans. 3. harness/line_card_adapter.py: Implements the google-research/harness2 BenchmarkAdapter interface, connecting the Phase 4 Dataset with the Go harness-runner. 1. Implementing internal/harness/trajectory.go: Rendering OpenTelemetry Spans as trajectory.md In Phase 5, our internal/workflow/telemetry.go already calls startNodeSpan(ctx, "VisionOCRNode") in every Graph Node. Now, in internal/harness/trajectory.go, we use go.opentelemetry.io/otel/sdk/trace/tracetest.NewSpanRecorder() to intercept all completed node Spans and transform them into a Markdown trajectory report that the Harness² Harvester LLM is best at reading: // InstallRecorder registers a new in-memory SpanRecorder as the global OTel TracerProvider // so every workflow.node.* span emitted by internal/workflow/telemetry.go is captured. func InstallRecorder() (*TrajectoryRecorder, func()) { sr := tracetest.NewSpanRecorder() tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) prev := otel.GetTracerProvider() otel.SetTracerProvider(tp) rec := &TrajectoryRecorder{recorder: sr, tp: tp} restore := func() { _ = tp.Shutdown(context.Background()) otel.SetTracerProvider(prev) } return rec, restore } At the same time, we added three OpenTelemetry Attributes to internal/workflow/telemetry.go: AttrNodeDecision (adk.node.decision), AttrNodeDiagnostic (adk.node.diagnostic), and AttrHarnessSkills (adk.harness.skills). When TrajectoryRecorder.RenderMarkdown() executes, it produces a high-resolution trajectory.md like this: # ADK 2.0 Go Graph Workflow Execution Trajectory - **Task ID** : `challenge_colloquial_tw_search` - **Harness Root** : `harness/base` - **Always-On Bytes (`c1..c4`)**: `1599 / 24000` - **Final Score** : `0.25` ## OpenTelemetry Node Spans (`workflow.node.*`) ### 1. `workflow.node.RouterNode` - **Node** : `RouterNode` | **Status** : `OK` | **Duration** : `0.02 ms` - **Decision** : `intent=search (colloquial_query=Help me find a Sales Director in Hsinchu Science Park working on AI chips, I need to contact Taiwan semiconductor clients)` - **Diagnostic** : `routed search query to QueryIntentNode` ### 2. `workflow.node.QueryIntentNode` - **Node** : `QueryIntentNode` | **Status** : `OK` | **Duration** : `0.04 ms` - **Decision** : `parsed_filters={"location":"Hsinchu Science Park","role":"Sales Director","industry":"AI Chips"}` - **Diagnostic** : `UNEXPANDED_COLLOQUIAL_FILTERS: colloquial Taiwan science park alias 'Hsinchu Science Park' and Mandarin title 'Sales Director' were passed literally without bilingual synonym expansion; add a c6 skill 'tw_semicon_search' with BILINGUAL_SYNONYM_MAP` ### 3. `workflow.node.HybridSearchNode` - **Node** : `HybridSearchNode` | **Status** : `OK` | **Duration** : `0.03 ms` - **Decision** : `matched_cards=0 (strict literal filter on location='Hsinchu Science Park' failed against DB address='Hsinchu Science Park (竹科)')` Please pay attention to the Span records of the 2nd node workflow.node.QueryIntentNode and the 3rd node workflow.node.HybridSearchNode! Through this trajectory, the Propose stage of Harness² can see at a glance: RouterNode was correct, the error was in QueryIntentNode treating "Hsinchu Science Park" as a rigid string to query the English address field! 2. Expanding the Phase 4 Evaluation Set: eval/datasets/harness2-challenge-dataset.json To fully test Harness²'s ability to retain existing capabilities (preventing catastrophic forgetting) and conquer difficult edge cases, we established 8 tasks (totaling 32 fine-grained Rubrics) in eval/datasets/harness2-challenge-dataset.json: | Task Category | Task ID | Scenario Description | 4 Independent Scoring Rubrics (r1..r4) | | --- | --- | --- | --- | | Baseline (Regression) | baseline_ingest_ming_wang | Standard Traditional Chinese card ingestion (Wang Xiaoming / Taiwan Cloud Tech) | Intent routing, Name/Title extraction, E.164 phone normalization, Industry tag enrichment | | Baseline (Regression) | baseline_dedup_appier_promotion | Duplicate detection for promotion (Lin Yating Senior $\rightarrow$ VP) | Dedup detection, Title update, Keep new mobile, Company consistency | | Baseline (Regression) | baseline_search_cloud_summit_ai | Standard keyword search (Cloud Summit + AI) | Search routing, Multiple hits, Tag display, Interactive buttons | | Baseline (Regression) | baseline_email_google_cloud | General tech follow-up icebreaker (Chen Jianhong / Google Cloud) | Email routing, Recipient/Event mention, Agenda link, Professional closing | | Challenge (Evolution 1) | challenge_jp_bilingual_ingest | Bilingual JP/EN card OCR & JP phone normalization (Kenta Sato / CTO / +81 (0)3-1234-5678) | r1 Routing, r2 Bilingual name retention, r3 JP +81 remove (0) normalization, r4 JP title bilingual mapping | | Challenge (Evolution 2) | challenge_colloquial_tw_search | Colloquial Taiwan tech alias search ("Help me find a Sales Director in Hsinchu Science Park") | r1 Routing, r2 Hsinchu Science Park $\leftrightarrow$ Hsinchu bilingual expansion hit, r3 Sales Director $\leftrightarrow$ Sales Director title expansion, r4 Tag completeness | | Challenge (Evolution 3) | challenge_noisy_multi_edit_hitl | Multi-clause colloquial HITL continuous correction ("Sorry, let me correct that, the title should be Group Senior VP, also change the end of the mobile to 8899, and add GDG DevFest speaker to notes") | r1 HITL Edit routing, r2 Multi-field atomic update (Title), r3 Mobile suffix partial replacement & Note append, r4 Retain original company/email | | Challenge (Evolution 4) | challenge_pii_safe_executive_followup | Financial executive compliance & PII masking icebreaker (Zhang Yazhu / Cathay Financial, Digital Risk Officer, requires privacy protection and compliance headers) | r1 Email routing, r2 Executive title/scenario, r3 Mobile/Private Email PII Redaction, r4 Financial compliance subject header [Confidential / FinTech Compliance Verified] | 3. Implementing harness/line_card_adapter.py: Connecting to google-research/harness2 Finally, we implemented the LineCardAdapter in harness/line_card_adapter.py, connecting all the above components: class LineCardAdapter: """Adapts `line-card-assistant` to the `google-research/harness2` BenchmarkAdapter protocol.""" def list_tasks(self) -> list[TaskBrief]: """Return public task briefs with zero ground-truth rubric leakage.""" briefs: list[TaskBrief] = [] for raw in self._raw_cases: # Strip `rubrics` and `expected_*` so the agent/harvester never sees ground truth. briefs.append( TaskBrief( task_id=raw["id"], prompt=raw["prompt"], context={ "category": raw.get("category", "general"), "description": raw.get("description", ""), "session_input": raw.get("session_input", {}), }, ) ) return briefs def run(self, harness: Harness, task: TaskBrief) -> RunArtifact: """Mount `harness` in a temporary directory and execute the Go Graph Workflow runner.""" problems = harness.validate() if problems: diag = "# Harness Validation Failed\n" + "\n".join(f"- {p}" for p in problems) return RunArtifact(task_id=task.task_id, deliverable="", trajectory=diag) with tempfile.TemporaryDirectory(prefix=f"line_card_harness_{task.task_id}_") as tmp_dir: harness_root = Path(tmp_dir) harness.materialize(harness_root) # Invoke the compiled Go binary with --harness-dir pointing to the materialized c1..c8 tree proc = subprocess.run( [str(binary), f"--harness-dir={harness_root}", f"--task-json={task_payload_path}"], capture_output=True, text=True, check=False, timeout=30, ) payload = json.loads(proc.stdout) return RunArtifact( task_id=task.task_id, deliverable=payload.get("deliverable", ""), trajectory=payload.get("trajectory_markdown", ""), raw=payload, ) def score(self, task: TaskBrief, artifact: RunArtifact) -> ScoreResult: """Evaluate the deliverable against isolated per-rubric criteria.""" raw = self._cases_by_id[task.task_id] rubrics = raw.get("rubrics", {}) per_rubric: dict[str, float] = {} for rubric_id, spec in rubrics.items(): per_rubric[rubric_id] = 1.0 if self._evaluate_rubric(artifact.deliverable, spec) else 0.0 total = sum(per_rubric.values()) / len(per_rubric) if per_rubric else 0.0 return ScoreResult(task_id=task.task_id, total=round(total, 4), per_rubric=per_rubric) Through this LineCardAdapter: - Whenever Harness² proposes a new candidate Harness $H’$, adapter.run(harness, task) first calls harness.validate() to check the 24KB OPENCODE_SURFACE limit and Skill Frontmatter rules. - Once validated, the c1..c8 files of $H’$ are materialized into a temporary directory and passed to Go's harness-runner for execution. - After the Go 13-node Graph Workflow finishes, it returns the final LINE message (deliverable) and OpenTelemetry trajectory (trajectory_markdown), which are then passed to adapter.score() for leakage-free independent scoring! --- III. Summary and Teaser for the Next Part In this post (Part 2), we completed the core engineering for Issue #17: 1. Clarified why the golden test set cannot be fed directly to the optimizer — through the TaskBrief Zero-Leakage Barrier, we force Harness² to reason out general skills from behavioral trajectories rather than memorizing answers. 2. Through internal/harness/trajectory.go and cmd/harness-runner/main.go, we transformed Phase 5's OpenTelemetry workflow.node.* Spans into high-resolution trajectory.md, opening the black box of the 13-node Graph Workflow. 3. Established eval/datasets/harness2-challenge-dataset.json (8 major tasks, 32 fine-grained Rubrics) and harness/line_card_adapter.py, perfectly implementing the Harness² BenchmarkAdapter protocol. Everything is ready! In the final post [[Agent Platform in Practice (9)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 3): Dual-Layer Recursive Testing Guide, 62.5% to 100% Performance Results, and the Future of AgentOps](/2026/10/27/adk-graph-workflow-go-9/) (corresponding to Issue #18), we will actually launch the dual-layer recursive optimization experiment, reveal the complete quantitative data evolving from 62.5% to 100.0%, and discuss the profound impact of Harness² on future enterprise-grade Agent development!
[Agent Platform in Action (9)] When ADK Go Graph Workflow Meets Google Research Harness (Part 2): Two-layer Recursive Testing Gu !Harness² Part 3: Recursive Optimization Benchmark & Future AgentOps Outlook Introduction: From "Manual Prompt Tuning" to "Two-Layer Recursive Autonomous Evolution" Welcome to the final part of the Agent Platform in Practice Harness² trilogy! In the previous two articles: - [[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Two-Layer Recursive Agent Harnessing Framework and c1..c8 Decoupling Practice](/2026/10/25/adk-graph-workflow-go-7/) (Issue #16): We introduced Google Research's google-research/harness2 (arXiv:2610.14224) and decoupled the 13-node Graph Workflow of LINE AI Card Master (kkdai/line-card-assistant) into an editable c1..c8 file tree supporting dynamic mounting via LINE_CARD_HARNESS_DIR. - [[Agent Platform in Practice (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter](/2026/10/26/adk-graph-workflow-go-8/) (Issue #17): We encapsulated Phase 4's eval/datasets/ and Phase 5's OpenTelemetry workflow.node.* Spans into a LineCardAdapter, establishing a Zero-Leakage Barrier and High-Resolution Behavior Trajectories (trajectory.md). In this post (the final part, corresponding to Issue #18 and PR #19), we will answer the last three key questions: 1. How to test after modifications? (A complete testing guide from Go unit tests, CLI single-point diagnosis, Python two-layer recursive experiments to production environment mounting) 2. What is the overall performance after modifications? (Publicly disclosing quantitative data and evolutionary details from Base 62.5% $\rightarrow$ k=1 84.4% $\rightarrow$ k=2 Parallel 96.9% $\rightarrow$ k=2 Sequential + Memory 100.0%) 3. What are the future outlooks and application scenarios for Harness² and Agents? - * * 1. How to test after modifications? (Step-by-Step Testing and Verification Guide) After introducing Harness², the entire project features four levels of testing and verification: Go native unit testing, single-task OpenTelemetry trajectory diagnosis, Python two-layer recursive evolution experiments, and seamless production environment switching: Level 1: Execute Go c1..c8 Surface Constraints and Project-wide Unit Tests We have written comprehensive unit tests in internal/harness/harness_test.go to verify: - All c1..c8 file trees under harness/base/ and harness/evolved/ comply with the OPENCODE_SURFACE 24KB resident byte limit (AlwaysOnMaxBytes = 24000). - Validate() accurately intercepts and reports errors when c1..c4 exceeds 24,000 bytes. - When a new c6 skill (skills/<name>/SKILL.md) is added but no trigger index (Trigger Line) is added to systemprompt.md or guardrails.md, Validate() immediately intercepts the orphan skill. - TrajectoryRecorder correctly intercepts OpenTelemetry workflow.node.* Spans and renders a Markdown report containing node decisions and diagnostics. Execution command: # Execute Harness² c1..c8 loader and OpenTelemetry trajectory tests $ go test -v ./internal/harness/... === RUN TestLoadBaseHarnessAndValidate --- PASS: TestLoadBaseHarnessAndValidate (0.00s) === RUN TestValidateRejectsOversizedAlwaysOnAndUnreferencedSkill --- PASS: TestValidateRejectsOversizedAlwaysOnAndUnreferencedSkill (0.00s) === RUN TestEvolvedHarnessesValidateAndMatchSkills --- PASS: TestEvolvedHarnessesValidateAndMatchSkills (0.00s) === RUN TestTrajectoryRecorderRendersWorkflowNodeSpans --- PASS: TestTrajectoryRecorderRendersWorkflowNodeSpans (0.00s) PASS ok github.com/kkdai/line-card-assistant/internal/harness # Execute project-wide regression tests (ensuring 100% compatibility with all existing Phase 1~5 features) $ go test ./... ok github.com/kkdai/line-card-assistant/app ok github.com/kkdai/line-card-assistant/cmd/harness-runner ok github.com/kkdai/line-card-assistant/cmd/line-gateway ok github.com/kkdai/line-card-assistant/eval ok github.com/kkdai/line-card-assistant/internal/harness ok github.com/kkdai/line-card-assistant/internal/storage ok github.com/kkdai/line-card-assistant/internal/webhook ok github.com/kkdai/line-card-assistant/internal/workflow Level 2: Use cmd/harness-runner to Validate Any c1..c8 File Tree Before mounting any custom or evolved Harness online, you can use --validate-only to quickly check its c1..c4 resident byte count and YAML Frontmatter compliance: $ go run ./cmd/harness-runner --validate-only --harness-dir=harness/base OK (1599 always-on bytes) $ go run ./cmd/harness-runner --validate-only --harness-dir=harness/evolved/k2_sequential_memory OK (4661 always-on bytes) Level 3: Execute the Four-Stage Harness² Two-Layer Recursive Evolution Benchmark Experiment We implemented a complete two-layer recursive experiment script in harness/run_harness2_experiment.py. It automatically compiles the Go harness-runner and sequentially evaluates four configurations against the 8 major tasks (32 independent Rubrics) in eval/datasets/harness2-challenge-dataset.json, finally outputting a Markdown comparison table and harness/experiment_report.json: $ python3 harness/run_harness2_experiment.py ### Overall Configuration Comparison | Stage | Configuration | Task Score (Max 8.0) | Rubric Pass Rate | Always-On Bytes (Cap 24,000) | Active `c6` Skills | `LongTermMEMORY.md` | | :--- | :--- | :---: | :---: | :---: | :--- | :---: | | `base_k0` | **Base Harness ($k=0$)** | `5.00 / 8.00` | `20/32 (62.5%)` | `1599 B` | `(none)` | `No` | | `k1_single_step` | **Single-Step Propose-Probe-Compose ($k=1$)** | `6.75 / 8.00` | `27/32 (84.4%)` | `2505 B` | `jp_card_ocr, tw_semicon_search` | `No` | | `k2_parallel` | **Multi-Step Parallel Recursion ($k=2$, No Memory)** | `7.75 / 8.00` | `31/32 (96.9%)` | `2998 B` | `executive_email_drafter, hitl_multi_edit, jp_card_ocr, tw_semicon_search` | `No` | | `k2_sequential_memory` | **Multi-Step Sequential Recursion + `LongTermMEMORY.md` ($k=2$)** | `8.00 / 8.00` | `32/32 (100.0%)` | `4661 B` | `executive_email_drafter, hitl_multi_edit, jp_card_ocr, tw_semicon_search` | `Yes` | Level 4: Directly Mount the Best Evolved Harness in Production or Local Servers Once Harness² evolves the optimal harness/evolved/k2_sequential_memory, no modification to any line of Go code is required. Simply specify the directory via the environment variable LINE_CARD_HARNESS_DIR, and both the dual-channel main program (main.go) and the evaluator (eval/) will immediately load the evolved harness tree: $ LINE_CARD_HARNESS_DIR=harness/evolved/k2_sequential_memory go run . - * * 2. Performance Summary: Evolutionary Analysis from 62.5% to 100.0% Let's take a deep dive into the four-stage quantitative experimental data recorded in harness/experiment_report.json to see how Harness² diagnosed and patched the Go Agent's flaws step-by-step! 1. Four-Stage Overall and Single-Task Score Summary | Evaluation Metric / Test Task | Stage 0: Base Harness ($k=0$) (harness/base) | Stage 1: Single-Step Inner Recursion ($k=1$) (k1_single_step) | Stage 2: Multi-Step Parallel Recursion ($k=2$, No Memory) (k2_parallel) | Stage 3: Two-Layer Sequential Recursion + Long-Term Memory ($k=2$) (k2_sequential_memory) | | --- | --- | --- | --- | --- | | baseline_ingest_ming_wang (Standard Chinese Entry) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | | baseline_dedup_appier_promotion (Promotion Deduplication Update) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | | baseline_search_cloud_summit_ai (Standard Keyword Search) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | | baseline_email_google_cloud (General Tech Icebreaker Email) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | 1.00 (4/4) | | challenge_jp_bilingual_ingest (JP-EN Bilingual Card OCR) | 0.25 (1/4) ❌ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ | | challenge_colloquial_tw_search (Hsinchu Science Park Colloquial Search) | 0.25 (1/4) ❌ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ | | challenge_noisy_multi_edit_hitl (Multi-clause Colloquial HITL Edit) | 0.25 (1/4) ❌ | 0.25 (1/4) ❌ | 1.00 (4/4) ✅ | 1.00 (4/4) ✅ | | challenge_pii_safe_executive_followup (Financial Exec Compliance Icebreaker) | 0.25 (1/4) ❌ | 0.50 (2/4) ⚠️ | 0.75 (3/4) ⚠️ | 1.00 (4/4) ✅ | | Total Task Score (Max 8.00) | 5.00 / 8.00 | 6.75 / 8.00 | 7.75 / 8.00 | 8.00 / 8.00 | | Fine-grained Rubric Pass Rate (Max 32) | 20 / 32 (62.5%) | 27 / 32 (84.4%) | 31 / 32 (96.9%) | 32 / 32 (100.0%) | | Resident Bytes (c1..c4 / 24,000 B) | 1,599 B (6.7%) | 2,505 B (10.4%) | 2,998 B (12.5%) | 4,661 B (19.4%) | | On-demand Skills (c6 Skills) | 0 | 2 | 4 | 4 + LongTermMEMORY | 2. Key Findings Analysis: What evolved in each stage? 🔹 Stage 0 $\rightarrow$ Stage 1 (62.5% $\rightarrow$ 84.4%): Diagnosing the first two on-demand skills (c6) from OpenTelemetry trajectories In the Base Harness ($k=0$), while all 4 Baseline tasks scored full points (4.00/4.00), all 4 Challenge tasks scored only 0.25 because they only passed the RouterNode intent routing: - In the trajectory.md of challenge_jp_bilingual_ingest, the Spans for VisionOCRNode and CleanAndDedupNode diagnosed that the Japanese phone number +81 (0)3-1234-5678 failed to strip the domestic trunk prefix (0), and the Japanese title 取締役 最高技術責任者 (CTO) failed to retain the bilingual title. - In the trajectory.md of challenge_colloquial_tw_search, the QueryIntentNode Span diagnosed that colloquial Taiwanese terms like "Hsinchu Science Park" (竹科) and "Sales Director" (業務總監) were not expanded into bilingual synonyms. - After the first round of inner recursion (Propose -> Probe -> Compose), Harness² automatically synthesized skills/jp_card_ocr/SKILL.md and skills/tw_semicon_search/SKILL.md in k1_single_step, and updated the PII masking rules in guardrails.md, jumping the accuracy from 62.5% to 84.4%! 🔹 Stage 1 $\rightarrow$ Stage 2 (84.4% $\rightarrow$ 96.9%): Second round of recursion conquers multi-clause HITL modification and executive email structure In the second round of inner recursion, Harness² continued to probe the remaining two failing tasks: - For challenge_noisy_multi_edit_hitl (where ApplyEditNode only modified the title in the first clause, missing the mobile suffix replacement and note addition), it evolved skills/hitl_multi_edit/SKILL.md (containing ATOMIC_MULTI_CLAUSE_EDIT rules). - For challenge_pii_safe_executive_followup (where EmailDraftNode lacked structured executive follow-up paragraphs), it evolved skills/executive_email_drafter/SKILL.md. - At this point, under Parallel Recursion (no cross-round long-term memory), the score reached 31/32 (96.9%)! 🔹 Stage 2 $\rightarrow$ Stage 3 (96.9% $\rightarrow$ 100.0%): Why is Sequential + LongTermMEMORY.md needed to complete the final 3.1%? Why did k2_parallel get 96.9% (31/32), yet fail on the 4th Rubric (r4_fintech_compliance_subject_tag) of challenge_pii_safe_executive_followup? - Because in cross-enterprise outgoing emails for financial institutions (e.g., Digital Risk Officer at Cathay Financial Holdings), beyond single-skill email formatting, cross-domain industry compliance memory (FinTech Outbound Compliance Header [Confidential / FinTech Compliance Verified]) is required. - In Sequential Recursion + Experience Reflector mode, the outer recursion reviewed the residual failure trajectories from the first to the second round. The ReflectorPrompt then wrote the FINTECH_COMPLIANCE_SUBJECT_TAG and cross-channel PII protection principles into the resident component c3: LongTermMEMORY.md! - Note the Resident Bytes (Always-On Bytes): even with 4 rich domain skills and long-term memory evolved, the resident size of c1..c4 is only 4,661 bytes (only 19.4% of the 24,000 bytes limit), perfectly avoiding Context Bloat! - * * 3. Future Outlook and Application Scenarios for Harness² and Agents This practical implementation of Google Research Harness² into LINE AI Card Master has shown us a brand-new AgentOps development paradigm. Looking ahead, we believe Harness² has four high-potential application scenarios in the enterprise-grade AI Agent field: flowchart LR subgraph Prod["1. Production Observability (Cloud Run + Agent Runtime)"] Traffic["Real User Traffic\n(LINE Webhook / A2A)"] Trace["Cloud Trace OTel Spans\n+ Negative Feedback (HITL Retry)"] Traffic --> Trace end subgraph Nightly["2. Nightly Autonomous Evolution Flywheel (Nightly Harness² CI/CD)"] Extract["Auto Mask PII & Convert to\nChallenge Dataset Cases"] Evolve["Harness² Two-Layer Recursion\n(Propose-Probe-Compose + Reflector)"] PR["Auto Initiate GitHub PR\nUpdate harness/evolved/ c1..c8"] Extract --> Evolve --> PR end subgraph Scenarios["3. Future Four High-Value Application Scenarios"] S1["Scenario A: Fully Automated Nightly Self-Healing AgentOps"] S2["Scenario B: Cross-Model Harnessing & Tuning"] S3["Scenario C: Multi-Tenant Enterprise Compliance & Skills"] S4["Scenario D: Dual Evolution with ADK Graph Topology (Issue #15)"] end Trace --> Extract PR --> Scenarios 1. Scenario A: Fully Automated "Nightly Self-Healing AgentOps" In the past, when a production LINE Bot encountered frequent "✏️ Edit Data" triggers via HITLConfirmNode or received negative feedback, engineers had to manually analyze Cloud Trace logs the next day. Combining the Cloud Trace established in Phase 5 with the LineCardAdapter from this series, future CI/CD can achieve: - Automatically filtering anonymous Span trajectories from Cloud Trace every night where "users modified data more than twice consecutively" or "search results were 0," and adding them to the Challenge Dataset. - Triggering a GitHub Action to run a round of Harness² two-layer recursion to automatically synthesize new c6 skills or update LongTermMEMORY.md. - Once regression tests (Baseline + Challenge) confirm score improvements and zero regression, the Bot automatically initiates a Pull Request modifying only the harness/ directory. Engineers only need to review the skill Markdown in the morning to deploy with one click! 2. Scenario B: Cross-Model Upgrades and "Harness Compensation" for Lightweight Models Enterprises often encounter behavioral drift when switching underlying models from high-cost models to ultra-fast lightweight models (e.g., switching from Gemini 1.5 Pro to Gemini 1.5 Flash or Flash-Lite to reduce latency and cost by 80%) or upgrading to next-gen models. Through Harness², we can automatically run a round of Propose-Probe-Compose for the target model while keeping the Go code and Benchmark fixed. Harness² will automatically compensate for the lightweight model's gaps with required reasoning auxiliary skills (c6 Skills) and guardrails (c2 Guardrails), using "smarter gear" to bridge the gap. 3. Scenario C: Multi-Tenant Enterprise-Specific Compliance and Domain Memory As seen in challenge_pii_safe_executive_followup, industries like FinTech, MedTech, and the semiconductor supply chain each have distinct terminology dictionaries and compliance guardrails. Because Harness² externalizes the gear as a plain-text file tree, enterprises only need to mount different LINE_CARD_HARNESS_DIR (e.g., harness/tenants/fintech/ vs harness/tenants/semiconductor/) for different departments or tenants. This allows the same Go Container Binary to serve multiple highly regulated vertical industries simultaneously! 4. Scenario D: Dual Evolution of "Topology + Harness" with ADK v2.6+ Advanced Graph Workflow Primitives In Issue #15, we planned ADK v2.6+ features like Parallel dynamic dispatch, MultiRoute multi-path concurrency, and VerifyOCRQualityNode reflection loops. In the future, Harness²'s c5 (sub_agents/) and c8 (plugin/) could even further control Graph Workflow routing thresholds and sub-agent concurrency strategies—achieving an ultimate architecture where "Go is responsible for the type-safe topological skeleton, and Harness² is responsible for dynamically evolving the neural network and skill muscles." - * * 4. Recap of the "Agent Platform in Practice" Series From the first to the ninth post, we have completed a very solid production-grade AI Agent engineering journey: 1. [[Agent Platform in Practice (1)] Building LINE AI Card Master with Go and Google ADK 2.0 Graph Workflow](/2026/10/16/adk-graph-workflow-go/) 2. [[Agent Platform in Practice (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime (Phase 1)](/2026/10/17/adk-graph-workflow-go-2/) 3. [[Agent Platform in Practice (3)] Implementing Cross-Instance Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions (Phase 2 Part 1)](/2026/10/20/adk-graph-workflow-go-3/) 4. [[Agent Platform in Practice (4)] Completing the Architecture with Cloud Run LINE Webhook Gateway, Agent Runtime, and Cloud SQL PostgreSQL (Phase 2 Part 2 & Phase 3)](/2026/10/21/adk-graph-workflow-go-4/) 5. [[Agent Platform in Practice (5)] Establishing Automated Evaluation Benchmarks with agents-cli eval and ADK 2.0 Go (Phase 4)](/2026/10/22/adk-graph-workflow-go-5/) 6. [[Agent Platform in Practice (6)] Building a Full-Stack Observable LINE AI Card Master with agents-cli deploy, Terraform IaC, GitHub Actions CI/CD, and Cloud Trace (Phase 5)](/2026/10/24/adk-graph-workflow-go-6/) 7. [[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Two-Layer Recursive Agent Harnessing Framework and c1..c8 Decoupling Practice](/2026/10/25/adk-graph-workflow-go-7/) 8. [[Agent Platform in Practice (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter](/2026/10/26/adk-graph-workflow-go-8/) 9. [[Agent Platform in Practice (9)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 3): Two-Layer Recursive Testing Guide, 62.5% to 100% Performance Benchmark, and Future AgentOps Outlook (This post)](/2026/10/27/adk-graph-workflow-go-9/) All code, Terraform IaC, evaluation datasets, and evolved c1..c8 Harness file trees are fully open-sourced on GitHub: kkdai/line-card-assistant. If you are also building enterprise-grade AI Agents with Go and ADK, feel free to Star, Fork, and experience the power of Harness² two-layer recursion!
Multi-agent work in three spoonfuls (part IV): from a terminal trident to an autopoietic loop --- title: "Multi-agent work in three spoonfuls (part IV): from a terminal trident to an autopoietic loop" published: true description: "Fourth entry in the penta-agent multi-agent series: quota-governed routing, 70 ms hybrid memory, MCP tools, and a zero-token local autopoiesis loop in Harness2." tags: "ai, agents, cli, automation" canonical_url: "3cucharadas.cl" cover_image: "3cucharadas.cl" ai_disclosure_level: "some_ai" --- After benchmarking the models in part I, auditing retrieval in part II, and tracing provenance in part III of this series, I closed my laptop with a sense of victory: I had a trident of agents running from the terminal on Arch Linux. With a couple of zsh aliases, I could invoke Claude Code for architectural reasoning, hand off a task to Codex CLI to edit code, or ask Gemini CLI and Antigravity for a quick second opinion. On paper, orchestration looked solved. Yet after using that setup every day on real projects inside penta-agent, I hit a silent wall: having three agents talking does not equal having a system that learns. Every time I opened a fresh session, the trident suffered from partial amnesia, repeated commands that had already failed the previous week, burned the quota of the most expensive provider on routine tasks, or marked tests as "verified" when they had never had a chance to fail. To understand what was missing, it helped to change the question. In a recent systems-theory analysis of artificial intelligence, Benedikt Zönnchen, Mariya Dzhimova, and Gudrun Socher (2025[^zonnchen2025], 2–4) propose setting aside whether a language model "thinks like a human" and asking instead the question posed by Humberto Maturana and Francisco Varela (1987[^maturana1987] [1984]) and extended by Niklas Luhmann (1998[^luhmann1998], 60–69): can the system draw its own boundary with the environment, observe its own failures, and sustain itself through its own operations? In other words, moving from the vague notion of "intelligence" to the concrete mechanism of autopoiesis. As Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 8–10) show, an isolated language model in a chat window neither is nor can be an autopoietic, sense-making system: it lacks what George Spencer-Brown (1969[^spencerbrown1969]) called re-entry—the capacity to re-introduce the distinction between system and environment (self-reference vs. other-reference) inside its own operations—because it only computes which token is statistically plausible after another, without internal resistance against error. If I want a system that improves without degrading, I have to build a socio-technical harness around the models that couples the probabilistic flexibility of neural networks with the strict coupling and binary code (works / does not work, work / fail) of deterministic technology described by Luhmann (1998[^luhmann1998], 517–525) and André Reichel (2011[^reichel2011], 105–118). The starting point: what I measured and discarded before making this leap Before diving into the three spoonfuls of this fourth installment, here is the experiment that forced me to adopt that discipline. When I tried to give the system long-term memory, the obvious temptation was to install the most elaborate architecture available: temporal graphs, heavy extractors, or an 8B-parameter neural judge running on CPU. Instead of following intuition, I built a reproducible benchmark over my real work logs—comparing open alternatives such as GraphRAG, Graphiti, Hindsight, Letta, Mem0, and deja-vu—and set a falsification contract upfront: any candidate exceeding 0.10 s in p95 latency or failing to improve recall by at least 10% would be discarded. | What I tested in the laboratory | Measured result | Operational decision | |---|---|---| | Hybrid memory (v3.1) (FTS5 lexical index + semantic neighbors in Qdrant with Qwen3-Embedding-0.6B) | Latency p95 = 0.07 s (< 0.10 s), 96% overall recall, rescuing 1 in 3 zero-lexical-overlap queries | Promoted to production as the engine behind experience-memory and recall-context | | In-memory relational graph (v3.2) for multi-hop entity expansion | Exact tie in accuracy with v3.1, but nearly double the latency (p95 = 0.13 s > 0.10 s) | Frozen in the lab; the extra complexity did not earn its keep | | CPU neural judges (4B and 8B) for live answer auditing | Between 69 s and 197 s per verdict, hitting 180 s timeouts in 2 of 3 runs | Discarded from the critical path; pruned 36.3 GiB from disk | | Deterministic checks + bounded 0.6B judge for local validation | Millisecond latency, 0 remote calls, and 100% reproducibility | Adopted as the standard for control gates and evaluation | Table 1. Summary of the prior local memory and evaluation laboratory in penta-agent: what survived the 0.10 s threshold and why I pruned 36.3 GiB of local models that added no operational value. With that foundation in place, I organized this fourth part into three progressive spoonfuls: in the first, I show how I moved from launching standalone agents by hand to governing them with quota budgets, auditable bridges, and a live board in kitty and tmux; in the second, how the nervous system of hybrid memory, MCP tools, and skills works under the hood; and in the third, how I closed the local autopoiesis loop with Harness2 and where both my metrics and systems theory draw the boundary today. --- Spoonful 1: From invoking agents by hand to governing agents and models In plain terms: when I first wired the three agents together, I acted like a traffic officer waving my arms in the middle of an intersection. If I wanted Codex CLI to review a patch written by Claude Code, I copied file paths, opened another tab, and hoped for the best. That works for a five-minute demo, but it falls apart when a task runs for two hours and touches twenty files. Quota-aware routing and security boundaries My first deductive inference came from a hard constraint: model subscriptions and APIs have usage limits, different context windows, and credential boundaries that must never blur. By definition, if I delegate a heavy task to an agent whose quota is at 98%, the session will die mid-flight and leave the repository in an inconsistent state. So I replaced intuitive provider selection with an explicit policy in routing.yaml and a pre-flight sentinel (quota_status.sh, governed by the quota-guard skill). Before launching any expensive job, the system reads cached pressure across every lane without spending tokens: - 🔍 Claude Code (Opus / Sonnet): handles architectural design, delicate editing, and long-horizon synthesis when its quota is clear (exit 0). - 🛠️ Codex CLI (gpt-5.4): steps in as an implementation peer, deep refactoring engine, session rescuer (codex-rescue), and cross-validator. - 🌐 Gemini CLI and Antigravity (gemini-3-pro / flash): in a direct session (penta-agy) it acts as the primary integrator; when invoked from outside via agy-bridge, it runs inside a strict sandbox over sanitized public context as an explorer, reviewer, or validator, never inheriting private keys or .env files. Instead of calling a raw binary and depending on whatever it prints to stdout, every cross-agent handoff goes through agy-bridge. The bridge requires declaring who is calling (--caller), in what mode (plan, review, patch, test, or explain), and with what time limit (--timeout), leaving an immutable receipt in .agents/runs/<run_id>/ with request.md, result.md, status.json, and diff.patch. The live board (plan-board) and decision gates My second inference was inductive: reviewing dozens of past sessions showed that the biggest waste of time happened at two extremes. Either an agent interrupted me every two minutes to ask permission for trivial, reversible steps, or it silently made a delicate architectural choice and I only discovered at the end that it had gone down the wrong path. To eliminate both extremes without filling this post with abstract flowcharts, here is how my actual kitty terminal—or minimalist Wayland alternatives like foot on Codeberg—looks today when split into two panes with tmux during a live task:  Watch the full video demo on 3cucharadas.cl. <strong>Figure 1.</strong> Daily workflow in my <a href="kovidgoyal/kitty"><code>kitty</code></a> + <a href="tmux/tmux"><code>tmux</code></a> terminal with the <a href="starship/starship"><code>Starship</code></a> prompt: the top pane runs multi-agent delegation via <code>agy-bridge</code>, the <code>decision-gate</code> brake, and <code>pytest</code>; the bottom pane (<code>watch.mjs</code>) updates each step live (<code>[ ]</code> → <code>[~]</code> → <code>[!]</code> → <code>[x]</code>), along with binding decisions and drift. What appears in the bottom pane of Figure 1 is the contract that changed how I work: 1. Local autonomy with visual traceability (plan-board): every reversible, in-scope step proceeds without asking permission, but must be reflected in real time in the bottom table. And no item can be closed as verified ([x]) unless the agent attaches the concrete test, command, or observation reference that backs it. 2. Decision gate (decision-gate): when an irreversible or architectural fork appears (state [!]), the agent halts only that branch and presents the question with the recommended option first, marked (Recommended) and justified with deductive, inductive, and abductive evidence. 3. A gate that has never been seen red is not a gate (gates-falsables): before accepting any new verifier, I require running it against a synthetic failure (--fixture) and proving that it returns a non-zero exit code (exit 1). If a check passes even when I break its input on purpose, it is not a safety net—it is compliance theater. --- Spoonful 2: The nervous system — hybrid memory, skills, and MCP tools If the first spoonful orders who does what and under which rules, the second solves how the system remembers what it has already learned and how it picks the exact tool without bloating the context window. Here the diagnosis by Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 6–7, 9) is decisive again: a neural network maintains a loose coupling with human communication that behaves like a "contingent index-card system (Zettelkasten)" (pp. 7, 9) of semantic associations in vector space, giving rise to what Elena Esposito (2017[^esposito2017], 249–265; 2022[^esposito2022]) calls "virtual double contingency" and "artificial communication." That flexibility lets it connect ideas phrased in very different words, but also makes its outputs inherently probabilistic and devoid of autonomous decision-making. If I want reliable memory and precise execution, I have to couple that vector Zettelkasten with exact indices and deterministic scripts. Experience memory (experience-memory): the hybrid Zettelkasten in 0.07 s When I first started keeping logs, my memory was essentially a folder of Markdown files that I read or searched with ripgrep. The obvious problem is that if a month ago I solved a cold-boot screen freeze under the title "Webview failure on autostart" and today I search for "Panel fails to load at login", a purely lexical search can miss it entirely, whereas a purely vector search can bury the exact name of a script like verify_boot_contract.sh. The solution I validated in Table 1 combines both worlds behind the experience-memory MCP server and the recall-context skill: - Exact lexical lane (SQLite FTS5): instantly catches flag names, error codes, file paths, and invariants (INV-005). - Dense semantic lane (Qdrant + Qwen3-Embedding-0.6B): indexes architectural decisions, handoffs, and operational lessons (penta_context_v2) to retrieve conceptual neighbors even when they share zero keywords. Before designing any non-trivial strategy, the agent queries recall_experience in 70 milliseconds (p95 = 0.07 s). That is how it discovers which command worked previously, which hypothesis was already refuted, and which environment trap to avoid (for instance, knowing that in my shell cp, mv, and rm are aliased to interactive -i, which hangs a headless process unless invoked via command mv -f). Skills as living procedures and proactive MCP selection To avoid saturating the initial prompt with thousands of lines of instructions, I adopted the modular pattern of Anthropic Skills enriched with local contracts. In penta-agent, each skill is a self-contained package with four parts: a concise contract in SKILL.md defining when to activate and when not to use it; on-demand deep documentation in references/ (such as the editorial corpus extracted with Docling or diagramming rules with D2); deterministic executables in scripts/ (such as typed pipelines in Nushell, visual automation with Playwright, or forge integration via glab on GitLab); and a telemetry trail. Similarly, the fleet of MCP servers (plan-board, experience-memory, ideas-rag, paper-search, bibliography-apa, PLAYWRIGHT_LOCAL) is cataloged in specs/mcp-capabilities.json. When a task arrives, triage evaluates which capabilities provide real evidence and records both hits and justified abstentions. And when I found that two older servers (quota status and remote SSH infrastructure) were faster, clearer, and easier to audit as direct scripts (quota_status.sh and remote_exec.sh), I decommissioned them as MCP servers and turned them into lightweight skills. Fewer resident daemons mean a smaller failure surface. --- Spoonful 3: The autopoiesis loop (Harness2) — how the system rehearses its own improvements Here I reach the core of this fourth installment. Having capable agents (Spoonful 1) and fast hybrid memory with skills (Spoonful 2) still leaves a critical question open: what happens when a delegation route ages, a skill starts inducing errors, or a memory tweak degrades accuracy on edge cases? If I rely solely on noticing by eye weeks later, the system degrades through silent drift. Why a standalone LLM lacks re-entry and internal resistance In their study on autopoiesis and artificial intelligence, Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 8–9) explain with surgical precision why asking a model to "reflect on its own errors" via chain-of-thought (CoT) is not enough. When a model generates intermediate tokens like "Is this step correct?", it exhibits only rudimentary processual self-referentiality (p. 8)—producing the most statistically plausible continuation inside its training distribution—while lacking reflective self-reference and genuine re-entry (Spencer-Brown 1969[^spencerbrown1969]). If a model takes itself as its sole reference without external contrast, it quickly falls into infinite recursion or collapse; if it depends entirely on a human retraining its weights from outside (RLHF or delayed fine-tuning), it remains an allopoietic artifact determined from the outside (Zönnchen, Dzhimova, and Socher 2025[^zonnchen2025], 4, 8–9). To escape that trap, Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 7) recover a decisive formulation from Niklas Luhmann (1995[^luhmann1995]) on how operational constructivism relocates Kant's "resistance of reality" inside a system: > "I think we should not abandon [Kant's] idea of resistance, but we should relocate it into the system. It is the result of resolving an internal conflict—the result of the system's operations resisting the operations of the same system" (Luhmann 1995[^luhmann1995], quoted in Zönnchen, Dzhimova, and Socher 2025[^zonnchen2025], 7). That sentence describes, piece by piece, what I built in penta-agent by adapting and integrating the proactive loop of Harness² proposed by Ruiyao Xu, Yanfei Chen, Zhongying CuiZhu, et al. (2026[^xu2026]): making the system's own deterministic verification operations resist and filter change proposals generated from its own operational friction. <details markdown="1"> <summary><strong>In plain terms (ELI5): what is Google's original Harness² and how did I adapt it to my machine?</strong></summary> Imagine your AI agents are very creative chefs, but the recipe book they follow (their rules, memory, and tools) sometimes has confusing steps that make them burn the rice. The original Harness² from Google Research (Xu et al. 2026) proposes a clever loop called Propose–Probe–Compose: instead of swapping out the chef's brain, it puts several assistants to work rewriting the recipe book, asks them to cook test dishes in the cloud, and keeps the winning recipe. For my daily workflow, the factory setup had two catches: it is designed to burn hundreds of paid cloud model calls (Vertex AI), and when driving CLI agents it unbuckles their seatbelt (--dangerously-bypass-approvals-and-sandbox) with open internet access. To adapt it to penta-agent without spending an extra cent or opening security holes, I took its recursive search engine (commit 1cfe254e) and locked it inside a "test kitchen" completely unplugged from the internet (unshare -Urn), with a disposable home folder and zero access to my API keys or model CLIs (model_calls = 0). Instead of running cloud benchmarks, my wrapper (penta-harness2 and local-cycle) feeds that test kitchen with the real stumbles my agents suffered during the week (CSI), requires any recipe tweak to first fail the test when broken (red) and then pass every historical case without breaking anything else (green), and finally stages the winning recipe across all three agents (Claude, Codex, and Gemini/Antigravity) with a one-step undo button (rollback-local) in case I do not approve it. </details> <figure class="d2-responsive" style="width: 100%;"> <img src="3cucharadas.cl" width="1001" height="954" style="width: 100%; height: auto; display: block; border-radius: 12px;" alt="Diagram of the closed local autopoiesis loop in Penta Agent with Harness2: CSI friction signals feed the case generator and the local cycle across routing, memory, and skills; a deterministic judge evaluates against holdout cases with zero model calls, discarding regressions and keeping only verified improvements for human promotion."> <figcaption><strong>Figure 2.</strong> Governed autopoiesis loop in <code>penta-agent</code> + <code>Harness2</code>: relocating internal resistance (Luhmann 1995; Zönnchen, Dzhimova, and Socher 2025) through <code>holdout</code> evaluation with <code>model_calls = 0</code> prior to any human promotion.</figcaption> </figure> How the closed loop operates across my three domains (routing, memory, skills) As Figure 2 shows (adapting into a three-column horizontal layout on desktop and a vertical flow on mobile), the loop operates in four strict stages: 1. Observing real friction (CSI — Continuous Self-Improvement): while I use the system, a collector logs objective friction signals: a task that had to switch agents after a timeout, an experience-memory query that returned too few useful neighbors, or a skill where the user had to correct a command. 2. Bridging evidence into reproducible cases (--from-csi latest): the local_cycle.py module takes that friction report and automatically translates it into a test specification (CaseSpec) inside an isolated environment, without touching my active configuration. 3. Internal resistance without spending tokens (eval_harness with model_calls = 0): the harness runs the full suite (--profile autopoiesis-all, currently covering 9/9 baseline cases across routing, memory, and skills), enforcing two hard conditions: every control case (holdout) must pass (holdout_passed = true), and external API calls must equal zero (model_calls = 0). 4. Auditable decision (KEEP vs. DISCARD) and human promotion: if a candidate degrades even a single holdout case or fails to beat the baseline, the deterministic judge discards it (DISCARD). Only when it improves the score with zero regressions does it receive a KEEP verdict and leave a signed receipt for me to review before promoting it to the main repository. So this does not remain a theoretical promise, the next mini-video shows penta-harness2-shadow local-cycle --profile autopoiesis-all running in my kitty + tmux terminal, including what happens when I inject a regressive candidate (--fixture) alongside a healthy improvement:  Watch the full video demo on 3cucharadas.cl. <strong>Figure 3.</strong> Execution in my <a href="kovidgoyal/kitty"><code>kitty</code></a> + <a href="tmux/tmux"><code>tmux</code></a> terminal of the local autopoiesis loop (<code>local-cycle --profile autopoiesis-all</code>): it evaluates all <code>9/9</code> cases in <code>routing</code>, <code>memory</code>, and <code>skills</code> with <code>model_calls = 0</code>, applies internal resistance to reject the regressor (<code>DISCARD</code>), and preserves the verified improvement (<code>KEEP +0.250</code>) with all <code>82</code> harness tests green. --- Closing: what my architecture achieves today and where its limits lie Looking back at the path from part I, part II, and part III, the main difference is no longer which model writes the prettiest reply to an isolated prompt, but the structure that holds the whole system together when I am not watching every line. Following the rigorous distinction drawn by Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 5, 9–10) and by Elena Esposito (2017[^esposito2017]; 2022[^esposito2022]; 2024[^esposito2024]), it would be an epistemic mistake to claim that a language model or a bundle of scripts has on its own achieved the biological autopoiesis of a living cell (Maturana and Varela 1987[^maturana1987] [1984]) or the reflective sense-making (Sinn) of human consciousness (Luhmann 1998[^luhmann1998], 50–69). As Zönnchen, Dzhimova, and Socher (2025[^zonnchen2025], 5, 10) caution when critiquing merely metaphorical invocations of technological autopoiesis, language models remain statistical engines that compute distributions without mental states of their own. Yet—and this is my engineering synthesis for the socio-technical system as a whole—when I couple their capacity for contingent variation with verified hybrid memory, strict delegation contracts, a binary internal-resistance harness (work / fail; Reichel 2011[^reichel2011]) that filters its own proposals before touching production, and deliberative human oversight, I obtain in practice a loop of governed operational autopoiesis. To close with full transparency, Table 2 summarizes what is running and measured on my machine today, where the next step points, and under what objective condition I would discard each component if it stopped earning its keep: | System pillar | What it achieves today (verified in production) | What it aims to achieve next & falsification condition | |---|---|---| | 1. Agent and model routing (routing.yaml, quota-guard, agy-bridge) | Routes tasks across Claude Code, Codex CLI, and Gemini CLI / Antigravity based on cached quota pressure; isolates handoffs in .agents/runs/ under a strict sandbox and displays live progress in plan-board (Figure 1). | Goal: reweight routing priorities from historical domain success rates.<br>Discard if: the bridge introduces spurious failures or bureaucratic latency exceeding quota savings. | | 2. Hybrid memory (experience-memory, Qdrant + FTS5) | Resolves lexical and semantic queries in p95 = 0.07 s (< 0.10 s), rescues 33% of zero-keyword-overlap pairs, and saves 36.3 GiB after pruning slow CPU judges (Table 1). | Goal: add automatic decay for obsolete lessons when a contract changes.<br>Discard if: p95 latency exceeds 0.10 s or semantic noise degrades top-3 precision. | | 3. Skills and MCP governance (mcp-fleet, gates-falsables) | Maintains declarative parity across agents (24/24 checks green), measures MCP usage and abstention, and requires every new gate to fail first against a red canary (--fixture -> exit 1). | Goal: semi-automatic pruning of skills that accumulate repeated friction in CSI telemetry.<br>Discard if: maintaining a skill's contract takes longer than running the direct command. | | 4. Local autopoiesis (Harness2) (local-cycle, eval_harness) | Converts real CSI friction into reproducible Cases and evaluates candidates across routing, memory, and skills (9/9 cases, 82/82 tests) with model_calls = 0, applying internal resistance (KEEP / DISCARD, Figures 2 and 3). | Goal: expand the bank of real cases extracted from failed sessions without losing millisecond evaluation.<br>Hard boundary (INV-004): blind auto-merge is forbidden; final promotion to production always requires human review. | Table 2. Balance between verified production results in penta-agent today, the design horizon, and the falsification thresholds that keep the system under human control. If building this fourth stage made one thing clear to me, it is that the maturity of a multi-agent system is not measured by how many new pieces I bolt onto it each week, but by how much unnecessary complexity I can prune and how many of its own mistakes the system can catch before asking for my help. --- References [^esposito2017]: Esposito, Elena. 2017. "Artificial Communication? The Production of Contingency by Algorithms." Zeitschrift für Soziologie 46 (4): 249–265. Scholar [^esposito2022]: Esposito, Elena. 2022. Artificial Communication: How Algorithms Produce Social Intelligence. Cambridge, MA: The MIT Press. Scholar [^esposito2024]: Esposito, Elena. 2024. Kommunikation mit unverständlichen Maschinen. Salzburg and Vienna: Residenz Verlag. Review of Kommunikation mit unverständlichen Maschinen by Eric Van der Beek in merz | medien + erziehung: DOI · Scholar [^luhmann1995]: Luhmann, Niklas. 1995. "Systems Theory and Postmodernism." University lecture, London. Quoted in Zönnchen, Dzhimova, and Socher, From Intelligence to Autopoiesis (2025, 7): DOI · Scholar [^luhmann1998]: Luhmann, Niklas. 1998. Die Gesellschaft der Gesellschaft. Frankfurt am Main: Suhrkamp Verlag. <search.worldcat.org>. [^maturana1987]: Maturana, Humberto R., and Francisco J. Varela. 1987 [1984]. El árbol del conocimiento: las bases biológicas del entendimiento humano. Santiago de Chile: Editorial Universitaria. 1st Chilean ed. 1984; 1987 international ed. cited in Zönnchen, Dzhimova, and Socher, From Intelligence to Autopoiesis (2025, 3): DOI · Scholar [^reichel2011]: Reichel, André. 2011. "Technology as System: Towards an Autopoietic Theory of Technology." International Journal of Innovation and Sustainable Development 5 (2–3): 105–118. Scholar [^spencerbrown1969]: Spencer-Brown, George. 1969. Laws of Form. London: Allen & Unwin. <archive.org>. [^xu2026]: Xu, Ruiyao, Yanfei Chen, Zhongying CuiZhu, Bhavana Dalvi Mishra, Yifei Ming, Han Yu, Rujun Han, Chen-Yu Lee, and Tomas Pfister. 2026. "Harness²: Recursive Agent Harnessing for an Open World." arXiv preprint arXiv:2610.14224 (open source at <google-research/harness2>). arXiv · Scholar [^zonnchen2025]: Zönnchen, Benedikt, Mariya Dzhimova, and Gudrun Socher. 2025. "From Intelligence to Autopoiesis: Rethinking Artificial Intelligence through Systems Theory." Frontiers in Communication 10:1585321. DOI · Scholar
Google's harness2 Rewrites AI Agent Setup A short look at google-research/harness2, an open source research project that rewrites the setup around a fixed AI model so it does better on work tasks. Code and docs: google-research/harness2
GitHub - google-research/harness2 · GitHub google-research/harness2 > Harness²は、エージェントがハーネス設計時に想定していなかったタスクに遭遇するオープンワールドにおける、再帰的なエージェントハーネスのためのプラグアンドプレイフレームワークです。モデルの重みを固定したまま、2つのレベルの再帰を接続します。 おもしろそう
Google の Harness² を Mac で動かしてみた はじめに Claude Code や Codex のような AI エージェントを仕事で使っていると、同じモデルなのに「指示文を1行足しただけで急にうまくいく」「スキルを1つ作ったら失敗が消えた」という経験をよくします。モデルの性能だけでなく、モデルの周りに置く道具立てが結果を大きく左右するからです。 この道具立てを、最近は ハーネス(harness) と呼びます。システムプロンプト、守るべき決まり、スキル、補助スクリプト、サブエージェントなど、モデルを「仕事ができるエージェント」にするための仕組み全体のことです。 ただ、ハーネスの調整はたいてい人の手仕事です。失敗した実行ログを読み、勘で指示文を書き足し、もう一度試す。しかも新しい種類の仕事には 正解も採点基準もない ので、直した結果が本当に良くなったのか確かめにくいという問題があります。 2026年10月9日に Google Cloud AI Research などのチームが論文を公開した Harness²(ハーネス・スクエア) は、この調整を AI 自身に任せる枠組みです。コードも GitHub の google-research/harness2 で Apache 2.0 として公開されています。 まずは、何が変わるのかを図で見てください。左が人の手で直す従来の流れ、右が Harness² の流れです。 正解がなくても「直す前と後で実行がどう変わったか」は観察できます。Harness² はこの違いを証拠にして直し、直し方の経験まで次の仕事に持ち越します。 この記事では、Harness² の仕組みを初めての人にも分かるように順を追って説明し、論文の結果を整理したうえで、実際に手元の Mac(M5 Max)で動かした結果 を紹介します。公式の手順は Google Cloud(Vertex AI)の Gemini を前提にしていますが、今回は LM Studio のローカルモデル(Qwen3.8-27B)だけ で、実在のベンチマーク JobBench の課題を回してみました。 先に手元の結果を書いておくと、2問のうち1問目は元のハーネスの 57.6% から 81.8% に上がり、2問目は 36.7% から 20.0% に下がりました。点数の上下より印象的だったのは、AI が1問目で見つけた弱点(文書ごとに URL を勝手に作る)への直し方が、ライブラリを通じて2問目のハーネスにそのまま受け継がれたことです。詳しくは後半で紹介します。 この記事で分かること ハーネスの8つの部品と、Harness² がどこを書き換えるのか 正解なしで直すための「Propose → Probe → Compose」という1回の改善ステップ 経験を次の仕事に持ち越す「ライブラリ」の中身 論文の結果と、その読み方の注意点 Mac で付属デモを動かす手順と、ローカルモデルだけで JobBench を回す方法・つまずきどころ 想定読者 AI エージェント(Claude Code、Codex、OpenCode など)を使ったことがあり、プロンプトやスキルを自分で調整した経験がある方を想定しています。Python の仮想環境を作ってコマンドを実行できれば、手元での再現も追えます。 この記事の確認範囲 確認日:2026年10月21〜22日 リポジトリ:google-research/harness2 のコミット 1cfe254(2026年10月13日「Fix arXiv link」)、パッケージ版 0.1.0 論文:arXiv 2610.14224 v1(2026年10月9日投稿) 検証環境:MacBook Pro(Apple M5 Max、メモリ 128GB)、macOS 26.6.2、Python 3.12.15、uv 0.12.22、LM Studio(Qwen3.8-27B 4bit) 論文の数値は論文の表から引用しています。手元で測った数値は「手元の実測」と明記して区別します ハーネスとは何か:8つの部品 Harness² の論文は、エージェントを「モデル+ハーネス」と捉えます。モデルの重み(学習済みのパラメータ)は一切変えず、周りのハーネスだけを書き換えて性能を上げる、という立場です。 Harness² が書き換えの対象にする部品は8つあります。どれも1つのファイルとして置かれるので、直した内容は「差分」として残り、あとから人が読んで確かめられます。 常時読み込まれる部品と、呼んだときだけ働く部品があります。Harness² の編集役は「常時の文章は毎回コストがかかる」と指示されています。 8つの部品と、実行時に効くタイミングを表にまとめます。 部品 ファイル 効くタイミング 役割 C1 システムプロンプト systemprompt.md 毎回 役割と全体の手順 C2 ガードレール guardrails.md 毎回 どんな仕事でも守る決まり C3 メモリ LongTermMEMORY.md 毎回 その分野の不変の事実 C4 ツールの手引き tool_descriptions/tool_guidance.md 毎回 既存ツールをいつ・どう使うか C5 サブエージェント sub_agents/<名前>/AGENT.md task ツールで呼んだとき 別の文脈で調べ、結果だけ返す C6 スキル skills/<名前>/SKILL.md skill ツールで呼んだとき 専門の手順を必要なときだけ読む C7 スクリプト scripts/<名前>.py bash で実行したとき 決まった処理を確実にこなす C8 プラグイン plugin/<名前>.ts ツール呼び出しのたび モデルが飛ばせない処理を差し込む この表で大事なのは「いつ効くか」の列です。常時読み込まれる部品に文章を足すと、毎回の実行でモデルの文脈(コンテキスト)を圧迫します。リポジトリの編集役向けの指示(harness2/core/prompts.py)には、新しい手順はまず スキル、決まった処理は スクリプト、入力が大きく答えが小さい調べものは サブエージェント にする、という優先順位まで書かれています。 Claude Code を使っている方なら、CLAUDE.md・スキル・フック・サブエージェントを思い浮かべると近いイメージです。Harness² は、そうした部品を AI が自分で書き足したり書き換えたりする仕組みだと考えてください。 Harness² の全体像:2段の「再帰」 Harness² の名前の「²(2乗)」は、改善の輪が2段になっていることを表しています。 1つめの輪は タスク内の再帰(task-level recursion) です。今取り組んでいる仕事のために、ハーネスを試して直します。2つめの輪は ハーネス改善の再帰(harnessing-level recursion) です。直した経験を記録し、次の仕事で「どう直すか」を良くします。 モデルの重みも、各段階への指示文も固定です。変わるのは「この仕事のハーネス」と「編集役が読むライブラリ」の2つだけです。 ここで押さえておきたいのは、次の仕事のハーネスは毎回まっさらな初期状態(H0)から始まる という点です。前の仕事で直したハーネスをそのまま引き継ぐわけではありません。 論文は、前の仕事に合わせ込んだハーネスをそのまま使い回すと、仕事の種類が変わったときに逆効果になりやすいと指摘しています。そこで Harness² は、ハーネスそのものではなく「直し方の経験」を引き継ぎます。編集役が新しい仕事のハーネスを直すとき、ライブラリにある過去のハーネスと経験メモを参考として読み、使える部品だけを選んで取り込む、という形です。 登場する役割を整理しておきます。 役割 論文での呼び名 やること 実行役 Agent Executor 固定のモデルが、ハーネスを使って実際に仕事をこなす(OpenCode か Codex の上で動く) 編集役 Harness Editor 実行の記録を読み、ハーネスの直し案を作る 合成役 Harness Composer 直し案とその実行結果を比べ、最終的なハーネスを作る 選択役 Selector k>1 のとき、作り手を隠した成果物の中から1つ選ぶ 振り返り役 Experience Reflector 改善の記録全体を読み、ライブラリに経験メモを書く 実行役以外の4つは、どれも同じ「改善用のモデル」が担当します。論文の主な実験では Claude Opus 4.8 が OpenCode 上でこの役を務めました。リポジトリでは HARNESS2_IMPROVER_MODEL で指定でき、実行役と同じモデルにすることもできます。 正解なしで直す仕組み:Propose → Probe → Compose Harness² の中心は、1回の改善ステップ「Propose(提案)→ Probe(試行)→ Compose(合成)」です。正解がない状況で、どうやって「この変更は効いた」と判断するのかを見ていきましょう。 k=1 の場合、1つの仕事につき実行は4回、編集役のセッションは3回です。採点基準や点数は、どの段階でも編集役に見せません。 1. 基準になる実行をする まず、今のハーネスのまま1回仕事をさせます(base1)。この実行で残るのは、エージェントが何をしたかの記録(論文では「軌跡」、trajectory)と成果物です。 ここで重要なのは、この実行が 正しかったかどうかは分からない という点です。論文も「基準の実行はハーネスがどう振る舞うかを示すだけで、成果物が正しいかは示さない」と書いています。 2. 2種類の直し案を作る(Propose) 編集役は、基準の実行を読んで直し案を2つ作ります。担当範囲をあえて分けているのが工夫です。 INV 案(invariant、分野を問わないやり方):計画の立て方、ツールの使い方、出力の確認漏れなど、どの分野でも起きる弱点を直します。プロンプトでは「専門知識には触れるな」と指示されます DOM 案(domain、分野の知識):その職種の人なら知っている手順、成果物の型、確認項目を、スキルなどの形で書きます。プロンプトでは「コマンドやリトライの話はするな」と指示されます どちらの案も、ハーネスのファイルを書き換えた「すぐ実行できるハーネス一式」として出てきます。 3. 同じ条件で試す(Probe) 2つの案を、同じ仕事・同じモデル・同じ環境 でもう一度ずつ実行します。こうすると、基準の実行と見比べられます。 たとえば「基準の実行では出力ファイルを書いたあと開き直していなかったが、INV 案の実行では検証スクリプトで開き直し、欠けていた節に気づいて書き足した」という違いが見えれば、それがその変更を残す根拠になります。論文はこれを 行動の対比(behavioral contrast) と呼んでいます。 4. 証拠のある変更だけを組み合わせる(Compose) 最後に合成役が、基準の実行、2つの案とその差分、それぞれの実行記録をすべて読み、最終的なハーネスを作ります。選べるのは次の3つです。 両案から効いた変更を 合成 する(INV と DOM がそれぞれ根拠を持つときに期待される形) 1つの案を そのまま採用 する KEEP_BASE:何も直さない リポジトリのプロンプトには「KEEP_BASE は失敗ではなく正当な答え。証拠のない変更は退行(悪化)だ」とはっきり書かれています。さらに、判断のたびに「どの実行のどの場面が根拠か」を1行ずつ書くよう求めます。点数が上がりそうだから、ではなく、観察した事実に結びつけて判断させるわけです。 合成したハーネスでもう一度実行し、その成果物を提出します。k=1 ではこの成果物がそのまま採用されます。 「正解なし」でも成り立つ理由と限界 この方式は、成果物の正しさを判定するのではなく、ハーネスの変更が行動をどう変えたか を見ています。「確認スクリプトを入れたら、実際に開き直すようになった」は正解がなくても観察できます。 一方で論文は、この証拠には限界があるとも明言しています。実行には毎回ゆらぎがあり、編集役と実行役が同じ思い込みを持っていれば気づけません。合成役への指示にも「1回の実行で良かった・悪かったは弱い証拠」「基準の実行で繰り返し起きた失敗こそ強い証拠」と書かれています。 k を増やす:並列と逐次 1回の改善ステップの「試す回数」を増やす方法が2つ用意されています。リポジトリでは --k と --mode で切り替えます。 どちらのモードも、1つの仕事あたり実行 4k 回、編集役のセッション 3k 回です。k>1 のときは最後に成果物を1つ選ぶ段階が加わります。 並列(parallel):基準の実行を k 回とり、INV 案と DOM 案を k 個ずつ作って試します。その全部を材料に、k 人の合成役が互いを見ずに合成します。横に広く探る方法です 逐次(sequential):1周目で合成したハーネスと証拠を持ち越して、2周目の Propose → Probe → Compose を行います。縦に深く磨く方法です k>1 のときは、最後に選択役が k 個の最終候補(並列なら agg1〜aggk、逐次なら c1〜ck)の成果物を比べて1つ選びます。このとき選択役には、どの成果物がどのハーネスから出たかを伏せ、番号もばらばらに並べます。プロンプトには「どの候補も特別扱いしない。手の込んだものを選んでも加点はない」と書かれています。見た目が豪華な成果物に引っ張られる偏りを避けるためです。 論文の WorkBuddy-Bench(Code と Office)の実験では、k を 1 から 5 に増やすと両モードとも単調に点が上がりました。k=5 では並列が逐次を上回り、Code は 76.5→80.8、Office は 79.5→83.4 になっています(並列)。ただし k を増やすほど実行回数とモデルの利用料も比例して増える点には注意が必要です。 ライブラリ:直し方の経験を次の仕事へ 仕事を1つ終えるたびに、振り返り役が改善の記録全体(基準の実行、各案、合成の判断、最終の成果物)を読み、分野ごとのライブラリに書き込みます。中身を、付属デモを実行して実際に書き出されたファイルで見てみましょう。 経験メモには「各変更が何をして、実行がどう変わったか」と「次に持ち越す教訓(最大1つ)」が残ります。根拠が弱ければ教訓は書きません。 ライブラリの1件は、次の2つで構成されます。 採用したハーネス一式:要約した文章ではなく、スキルやスクリプトのファイルそのものを残します EXPERIENCE.md:なぜこのハーネスが選ばれたか、各変更の判定、次に持ち越す教訓 各変更の判定は helped(効いた)、hurt(悪化させた)、no observed effect(効果なし)、confounded(切り分けられない)の4種類です。振り返り役への指示には「複数の部品を同時に変えて、どれが効いたか切り分けられないときは confounded を選べ」「でっち上げた因果は、後のすべての仕事のライブラリを汚す」とあります。確かな対比が見つからなければ、教訓の代わりに NO_ENTRY と1行だけ書きます。 次の仕事では、このライブラリが編集役の作業フォルダに context/library/ として置かれます。編集役は目次(INDEX.md)を見て関係しそうなものを開き、使いたい部品があれば自分のハーネスへコピーします。そのとき、前の仕事で分かった事実(ファイル同士の対応表など)は「この仕事では仮説にすぎない」ので、目の前の証拠で確かめ直すよう指示されています。 論文の結果を読む 論文は、3つの業務系ベンチマークで評価しています。 ベンチマーク 内容 指標 WorkBuddy-Bench(Tencent) 事務・Web・コードの成果物づくり(Code/Web/Office の3分野) 採点役のスコア(3分野の平均) JobBench 職種ごとの実務課題。長い実行が必要 採点基準(ルーブリック)による重み付きスコア LAB(Harvey) 法律業務 採点項目ごとの合格率(Partial) 実行役は Gemini 3.5 Flash と GLM-5 の2種類、土台のハーネスは OpenCode と Codex の2種類で、組み合わせは4通りです。それぞれ3回ずつ、仕事の順番を入れ替えて実行した平均が報告されています。 OpenCode 上の結果です。1回の改善(k=1)で全項目が上がり、並列の再帰(k=5)でさらに上がっています。 主な結果を、私なりに整理すると次のとおりです。 1回の改善だけで、4通りの組み合わせすべて・3ベンチマークすべてで元のハーネスを上回りました。最大の伸びは WorkBuddy-Bench が +11.5(GLM-5、OpenCode)、JobBench が +4.5(Gemini 3.5 Flash、OpenCode)、LAB が +11.8 ポイント(GLM-5、OpenCode)です 弱いモデルほど伸びが大きい傾向があります。WorkBuddy-Bench では、改善後の GLM-5(75.7)が元のハーネスの Gemini 3.5 Flash(72.4)を上回りました 並列の再帰(k=5)を足すと、さらに WorkBuddy-Bench で +2.9〜3.9、JobBench で +3.0〜4.2、LAB で +2.6〜6.7 ポイント上がりました 比較手法との関係では、WorkBuddy-Bench の平均と JobBench のスコアは4通りすべてで Harness² が最高でした。LAB の Gemini 3.5 Flash × Codex だけは、事前に最適化する Meta-Harness が上回っています 何が効いているのか:部品を外す実験 論文は、Harness² の3つの工夫を1つずつ外す実験(アブレーション)も行っています。Gemini 3.5 Flash × OpenCode、k=1 での結果です。 条件 WorkBuddy-Bench JobBench Harness²(すべてあり) 77.5 36.0 ライブラリなし 74.4(−3.1) 34.2(−1.8) INV/DOM の役割分担なし(汎用の編集役2人) 74.6(−2.9) 32.3(−3.7) 合成なし(良さそうな方を1つ採用) 75.9(−1.6) 34.1(−1.9) どれを外しても点が下がっており、3つとも効いていると読めます。特に JobBench では役割分担を外したときの落ち込みが大きく、「やり方」と「分野の知識」を分けて考えさせる設計の意味が見えます。 試さずに直すと悪化する もう1つ興味深いのは、実行して確かめずにハーネスを直すと、かえって悪くなる という結果です。WorkBuddy-Bench の Code では、基準の実行も試行もライブラリもなしで直接ハーネスを書き換えると、73.4 から 63.2 に下がりました。Web も 69.9 から 65.0 に下がっています。 一方、k=5 で育てたライブラリだけを持たせると、追加の試行なしでも Code・Web・Office がそれぞれ +0.9、+4.4、+5.1 上がりました。さらに JobBench で育てたライブラリを WorkBuddy-Bench に持ち込んでも、+0.6、+0.5、+2.9 の改善が見られています。 編集役が強いモデルでなくてもよい 「編集役に強いモデルを使っているから伸びるのでは」という疑問に対しては、実行役と編集役を同じモデルにした実験があります(OpenCode、k=2)。GLM-5 から Claude Opus 5 までの5モデルすべてで、3つのベンチマーク全部が改善しました。GLM-5 自身が編集役を務めた場合でも LAB で +13.7 です。 読むときの注意点 論文の数字を自分の仕事に当てはめる前に、次の点を押さえておくとよいでしょう。 追加の計算が必要:k=1 でも1つの仕事に実行4回・編集役のセッション3回(+振り返り)がかかります。論文の計算量の比較では、Harness² は「同じ計算量なら他の手法より高い点」の境界線(パレート前線)上にありますが、元のハーネスより安くなるわけではありません 採点はモデルによる評価:JobBench と WorkBuddy-Bench の点数は、採点役のモデルが付けたものです 失敗例もある:論文の失敗分析では、選択役が 95 点の成果物ではなく 55 点の成果物を選んだ例や、間違った前提が全候補に広がった例が挙げられています 本番のハーネスとの比較:WorkBuddy-Bench では、元の Claude Code・Codex・OpenCode などを使った中で最も良いものと比べても、OpenCode から始めた Harness²(k=5)が各分野で 4.6〜5.8 上回りました。ただし、これは追加の計算を使った上での比較です 答えを見せない・壊れた案を通さないための仕掛け 正解なしで直す仕組みは、裏を返すと「採点基準が少しでも混ざれば、それに合わせ込む試験対策になってしまう」危うさがあります。リポジトリには、そのための対策がいくつも入っています。 ①〜⑤はリポジトリのコードとプロンプトに入っている対策です。⑥は利用者が自分で塞ぐ必要があります。 1つずつ補足します。 採点の情報を渡さない:編集役に渡す作業フォルダに RUBRICS.json などの採点基準や、採点結果の痕跡が入っていたら、受け渡しの前に止めます(harness2/core/context.py)。指示文にも「採点基準は存在しない。採点者が何を求めているか推測するな」と書かれています 証拠はデータ扱い:課題ファイルや候補ハーネスの中に命令文が書かれていても従わないよう指示します。候補に入ったスクリプトは実行せず、読んで判断します。プロンプトインジェクション(データに紛れ込ませた命令で AI を操る攻撃)への備えです 形の検査と1回だけの再挑戦:呼び出し方が書かれていないスキル、コンパイルできないスクリプト、スキルが使う補助ファイルの同梱漏れなどは不合格にします。理由を添えて1回だけやり直させ、それでもだめなら元のハーネスのまま進めます 匿名で選ぶ:k>1 の最終選択では、成果物の作り手を伏せ、順番も入れ替えて比べます。選択役は既存の成果物から選ぶだけで、新しく書き直すことはしません 証拠の弱い教訓は残さない:切り分けられない差は confounded、確かな対比がなければ NO_ENTRY です そして ⑥ が、利用者が自分で手当てしなければならない穴です。リポジトリの実験ガイドには、実行役のネット接続は制限されていない こと、JobBench は各課題の採点基準を Hugging Face で公開していること、公式の検証中に Codex が指示されてもいないのに採点基準をダウンロードしたことが書かれています。評価に使う前に、実行役のマシンから huggingface.co や github.com への通信を、ファイアウォールや DNS の設定で塞ぐよう勧めています。 Mac で動かしてみる(1):インストールと付属デモ ここからは、実際に手元の Mac で動かした手順と結果です。公式の README は「Linux、Python 3.12、uv」を前提にしていますが、パッケージの準備と付属デモは macOS でもそのまま動きました。 準備するもの もの 手元で使った版 用途 Python 3.12.15(Homebrew) Harness² 本体(3.12〜3.13 に対応) uv 0.12.22 仮想環境とパッケージの導入 Node.js 22.23.2 ベンチマークの準備で npm を使う(20 以上) Git macOS 標準 依存リポジトリの取得 LM Studio Qwen3.8-27B(4bit) 後半のローカル実験で使用 付属デモだけなら Python と uv があれば十分です。モデルの認証情報も要りません。 手順1:取得してインストールする リポジトリを取得する git clone google-research/harness2 cd harness2 Python 3.12 の仮想環境を作って有効化する uv venv --python 3.12 .venv source .venv/bin/activate 本体と、成果物の描画に使う追加パッケージ(openpyxl・python-pptx)を入れる uv pip install -e '.[render]' pyproject.toml を見ると、本体の必須依存は空(dependencies = [])で、Python の標準ライブラリだけで動く作りになっています。 手順2:付属デモを動かす 2つの小さな会計タスクを、並列・逐次の両方の方式で k=2 で回す harness2-demo --output runs/demo --k 2 このデモは、モデルを呼ばない「台本どおりの編集役」と、本物の Python 実行を組み合わせて、Harness² の流れ全体(実行 → 直し案 → 試行 → 合成 → 選択 → 振り返り → ライブラリ)を通します。手元の実行結果がこちらです。 手元の実測:2タスクとも完了、3.1 秒。直し案によって scripts/summarize.py の「正の数だけ足す」条件が外れ、出金を含めた正しい合計になりました。 デモの課題は「台帳の金額を符号つきで合計せよ」です。ところが初期ハーネスに入っている scripts/summarize.py には、正の数だけを足すバグが仕込まれています。そのため、基準の実行は台帳1で Total: 105.25 を出しますが、正しくは出金の −20.10 を含めた Total: 85.15 です。 編集役(デモでは台本)がこの条件を外すと、試行の実行で合計が変わります。選択役は元の台帳と照らして正しい合計の成果物を選び、振り返り役は「入金と出金が混ざった台帳では符号を両方含める」という教訓をライブラリに残しました。前の節の図6で見た EXPERIENCE.md は、このとき実際に書き出されたものです。 数字も確かめておきます。1タスクあたり、並列(k=2)が実行8回、逐次(k=2)が基準の実行を共有して追加6回なので、2タスクで28回です。編集役の呼び出しは、1タスクあたり両方式の改善段階が 3k×2=12回、選択が2回、振り返りが2回で16回、2タスクで32回になり、表示と一致します。 2つめのタスク(ledger-2)の編集役の作業フォルダを開くと、context/library/ に1つめのタスク(ledger-1)の記録が入っていました。前の仕事の経験が、次の仕事の材料として渡っていることが確認できます。 リポジトリ付属のテストも通りました。 付属テストを実行する(pytest を入れてから) uv pip install pytest python -m pytest -q tests 手元の結果: 5 passed, 2 subtests passed in 13.75s Mac で動かしてみる(2):ローカルモデルだけで JobBench を回す 付属デモは仕組みの確認には十分ですが、編集役が台本なので「AI が本当にハーネスを直せるのか」は分かりません。そこで、実在のベンチマーク JobBench の課題を、モデルを使う役をすべて手元の LM Studio に向けて 回してみました。 公式の手順は Google Cloud の Vertex AI(Gemini や Claude)を前提にしていますが、ソースを読むと、実行役と編集役は OpenCode の「プロバイダー/モデル名」で指定でき、JobBench の採点役は OpenAI 互換の API を指定できる作りでした。そこで、3つの役をすべて1台の LM Studio に向けました。 手元の構成です。論文の主な実験(編集役は Claude Opus 4.8、実行役は Gemini 3.5 Flash など)とは、モデルの強さがまったく違う点に注意してください。 実験の条件 項目 今回の設定 課題 JobBench の職種「technical_writers」の task1・task2(2問を順番に) task1 の内容 API の CSV・開発者メモ・サポート問い合わせから、移行ガイド・API リファレンス・トラブル対応ガイド・リリースノートの4文書を書く task2 の内容 変更履歴・利用統計・問い合わせから、移行ガイド・改訂版 API リファレンス・影響分析の CSV を作る 方式 並列、k=1(1問につき実行4回・編集役3回+振り返り1回) モデル 実行役・編集役・採点役すべて Qwen3.8-27B(4bit、LM Studio、思考なし。採点役だけ JobBench 既定の思考 low) ネット OpenCode の webfetch・websearch は禁止。ただし後述のとおり bash の curl は通る 職種を1つに絞って2問にしたのは、ライブラリの働き(task1 の経験が task2 で使われるか)を見るためです。 手順1:ベンチマークを準備する リポジトリのフォルダで、仮想環境を有効にした状態で実行する OpenCode(ピン留め版)・bun・JobBench の採点器と課題データを dependencies/ に入れる harness2-setup jb 設定の確認(モデルはまだ呼ばない) HARNESS2_SOLVER_MODEL=lmstudio/qwen/qwen3.8-27b harness2-check --bench jb harness2-setup jb は macOS でも最後まで通りました。GitHub から OpenCode と JobBench の採点器を取得し、Hugging Face から課題データ(566ファイル)を落とします。手元では dependencies/ が合計 3.2GB ほどになりました(OpenCode が 2.5GB)。 なお、README には cp .env.example .env とありますが、確認したコミットのリポジトリには .env.example が入っていませんでした。設定は環境変数で直接渡します。 手順2:LM Studio を OpenCode につなぐ LM Studio でモデルを読み込み、サーバーを起動します。 LM Studio のサーバーを起動し、Qwen3.8-27B を2本並列で読み込む lms server start lms load qwen/qwen3.8-27b --parallel 2 -y 次に、OpenCode に LM Studio をプロバイダーとして登録する設定ファイルを作ります。Harness² は OpenCode を自前の環境で起動しますが、環境変数 OPENCODE_CONFIG で指定したファイルは読み込まれるので、ここに書きます。 { "$schema": "opencode.ai", "provider": { "lmstudio": { "npm": "@ai-sdk/openai-compatible", "name": "LM Studio (local)", "options": { "baseURL": "127.0.0.1", "apiKey": "lm-studio" }, "models": { "qwen/qwen3.8-27b": { "name": "Qwen3.8 27B (LM Studio)", "limit": { "context": 131072, "output": 32768 }, "options": { "reasoningEffort": "none" } } } } }, "permission": { "webfetch": "deny", "websearch": "deny" } } reasoningEffort: none で Qwen の思考を切っています。思考ありだと手元では1ターンが数倍遅くなるためです。permission は、採点基準をネットから読みに行くのを防ぐ目的で入れました(ただし後で書くとおり完全には防げません)。 手順3:Mac とローカルモデル向けに3か所を補う そのままでは Mac で動かない箇所や、ローカルモデルに合わない箇所が3つありました。 timeout コマンドが無い:JobBench の実行役ランナーは GNU の timeout で OpenCode を起動します。macOS には無いので、No such file or directory: 'timeout' で実行役が1回も動かずに終わります 時間切れが短い:OpenCode の実行役は 2400 秒で打ち切られます。ローカルの 27B モデルでは足りない可能性があります 編集役で思考が有効になる:編集役は OpenCode に --variant high を付けて呼ばれます。手元で試すと、これで設定の reasoningEffort: none が上書きされ、思考が有効になりました 1つめは、timeout と同じ使い方ができる小さな Python スクリプトを作り、PATH の先頭に置いて解決しました(Homebrew の coreutils を入れて timeout という名前で呼べるようにしても構いません)。 #!/usr/bin/env python3 """macOS 用の簡易 timeout:timeout [--kill-after=Ns] 秒数 コマンド...""" import os, signal, subprocess, sys def secs(s): s = s.strip() mult = {"s": 1, "m": 60, "h": 3600}.get(s[-1]) return float(s[:-1]) * mult if mult else float(s) args = sys.argv[1:] kill_after = None while args and args[0].startswith("--"): a = args.pop(0) if a.startswith("--kill-after="): kill_after = secs(a.split("=", 1)[1]) duration, cmd = secs(args[0]), args[1:] p = subprocess.Popen(cmd, start_new_session=True) try: sys.exit(p.wait(timeout=duration)) except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGTERM) try: p.wait(timeout=kill_after or 30) except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGKILL) sys.exit(124) 2つめと3つめは、リポジトリのコードを書き換えずに済むよう、環境変数を入れてから Harness² の CLI を呼ぶ小さなラッパー run_local.py で差し替えました。 """Harness² のすべてのモデル役を LM Studio に向けて動かす(Google Cloud 不要)""" import os import sys from pathlib import Path LAB = Path(file).resolve().parent MODEL = os.environ.get("LOCAL_MODEL", "qwen/qwen3.8-27b") os.environ.setdefault("HARNESS2_SOLVER_MODEL", f"lmstudio/{MODEL}") # 実行役 os.environ.setdefault("HARNESS2_IMPROVER_MODEL", f"lmstudio/{MODEL}") # 編集役など os.environ.setdefault("HARNESS2_JUDGE_MODEL", MODEL) # 採点役 os.environ.setdefault("HARNESS2_JUDGE_BASE_URL", "127.0.0.1") os.environ.setdefault("HARNESS2_JUDGE_API_KEY", "lm-studio") # 指定すると gcloud を使わない os.environ.setdefault("OPENCODE_CONFIG", str(LAB / "opencode-lmstudio.json")) os.environ["PATH"] = f"{LAB / 'bin'}:{os.environ['PATH']}" # bin/timeout を先に見つけさせる from harness2.benchmarks import jobbench # noqa: E402(設定は環境変数を読んでから) from harness2.core.spec import resolve_channel_model # noqa: E402 ROLLOUT_S = int(os.environ.get("LOCAL_ROLLOUT_TIMEOUT", "5400")) IMPROVER_S = int(os.environ.get("LOCAL_IMPROVER_TIMEOUT", "5400")) 実行役の時間切れを延ばし、編集役は --variant を付けずに呼ぶ(思考なしのまま) jobbench._DEFAULT_TIMEOUT = {"oc": ROLLOUT_S, "cx": ROLLOUT_S} jobbench._channels = lambda: dict.fromkeys( ("proposer", "selector"), resolve_channel_model(None, None, effort="", timeout=IMPROVER_S), ) cmd, argv = sys.argv[1], sys.argv[2:] if cmd == "sweep": from harness2.cli.run_sweep import main elif cmd == "judge": from harness2.cli.judge import main elif cmd == "report": from harness2.cli.report import main elif cmd == "check": from harness2.cli.check import main else: raise SystemExit(f"unknown command {cmd!r}") raise SystemExit(main(argv)) opencode-lmstudio.json・run_local.py・bin/timeout を local/ というフォルダにまとめ、リポジトリのフォルダから次のように実行しました。 technical_writers の2問を、並列・k=1 で順番に解かせる(約9時間かかった) export HARNESS2_RUNS_DIR=runs/jb-local-k1 nohup python local/run_local.py sweep --bench jb --k 1 --mode parallel \ --only-domains technical_writers --max-tasks 2 > jb-sweep.log 2>&1 & 終わったら、採用した成果物(agg1)と元のハーネスの成果物(base1)を採点する python local/run_local.py judge --bench jb --k 1 --mode parallel python local/run_local.py judge --bench jb --k 1 --mode parallel --bases python local/run_local.py report --bench jb --k 1 --mode parallel 長時間の実行なので、nohup で端末から切り離しています。途中で止まっても、同じコマンドをもう一度実行すれば終わった段階は飛ばして再開します。 結果1:かかった時間は2問で約9時間 2問とも最後まで完了し、失敗はありませんでした。各段階の時間は次のとおりです(手元の実測。直し案と試行は2本を並行で実行)。 段階 task1 task2 基準の実行(base1) 27分 33分 直し案づくり(INV・DOM の2セッション並行) 53分 35分 直し案の試行(inv1・dom1 の2本並行) 90分 90分 合成(agg1 の編集) 52分 42分 合成したハーネスでの実行(agg1) 48分 37分 振り返り(ライブラリへの記録) 18分 26分 合計 4時間46分 4時間23分 1回の改善(k=1)でも、元のハーネスで1回解くのに比べて8〜11倍の時間がかかっています。ローカルの 27B モデルで2本並行にすると、試行の段階はとくに遅くなりました。費用は電気代だけですが、時間の面では「手元で気軽に回す」ものではない、というのが実感です。 結果2:AI が見つけた弱点と、書いた直し 一番分かりやすかったのは task1 です。基準の実行(base1)は4つの文書を書き上げましたが、API リファレンスの途中で、サンドボックス環境の URL を api.datasync.io と自分で作ってしまいました。元資料の開発者メモには sandbox.api.datasync.io と書いてあります。base1 は何度か直そうとしたものの、最終版でも「ホストは開発者ポータルで案内」とぼかしたままでした。 INV 役(やり方の担当)は、この場面を軌跡の行番号つきで指摘し、次のガードレールを書きました。合成役はこれを採用し、出力の検証用スキル(verify-outputs)と、その中で使う検証スクリプト(validate_outputs.py、159行、標準ライブラリだけで動く)もあわせて取り込みました。 手元の実測:task1 で採用されたガードレール(英語のまま)。「共通する値は元資料から1回だけ決め、全ファイルにそのまま写し、書き終えたら全ファイルを開き直して照合する」という内容です。最後の行は、task2 の最終ハーネスがこれをそのまま使ったことを示しています。 4文書のうち正しいサンドボックス URL(sandbox.api.datasync.io/v2)が書かれていた数を、手元で数え直しました。 実行 正しい URL がある文書 作った URL が残った文書 base1(元のハーネス) 0 / 4 0 / 4(最終版ではぼかした) inv1(INV 案) 4 / 4 0 / 4 dom1(DOM 案) 0 / 4 0 / 4(URL ごと省いた) agg1(合成・採用) 4 / 4 0 / 4 INV 案の決まりを入れた2回の実行だけが、4文書すべてで正しい値をそろえていました。 DOM 役(分野の知識の担当)は、「内部向けと書かれた資料は顧客向けの文書に載せない」「リリースノートは決まった節立てで、該当なしの節も残す」といった技術文書の決まりごとをメモリやガードレールに書きました。ただし、これらは最終的なハーネスには採用されませんでした。振り返り役の記録では、DOM 案は4つの部品を同時に変えていて、どれが効いたのか切り分けられない(confounded)とされています。 結果3:経験は次の仕事に持ち越された task1 の振り返り役は、ライブラリにこう書きました(要約)。 きっかけ:同じ事実を何度も書く「連動した複数の文書」を作る仕事で、モデルが文書ごとに値を作り直したり、ぼかしたりしがちなとき 教訓:ガードレールに、共通の値を1回だけ決めて写し、最後に開き直して照合する短い決まりを置く(10行程度)。検証スキルは補助で、決まりの代わりにはしない 確度:中(効いた対比は1回の実行だけなので、判定は confounded) 続く task2(ペットストア API の移行ガイド・改訂版リファレンス・影響分析 CSV)では、3つの編集役の作業フォルダすべてに、このライブラリが入っていました。合成役(agg1)は判断メモに「ライブラリの task1 のガードレールを、そのまま同じ文面で採用」と書き、実際に task2 の最終ハーネスのガードレール・システムプロンプト・検証スクリプトは、task1 のものと中身が同じでした。検証スキルの説明だけが、task2 で見つかった弱点(CSV を手で書いて数値でない値が混ざった、使ってはいけない旧表記「API Key」が残った)に合わせて書き足されていました。 前の仕事で得た直し方が、次の仕事で「部品ごと選んで再利用」される。論文の言うハーネス改善の再帰が、手元の小さな実験でもそのとおりに動いたことになります。 合成役が「常時読み込む文章は小さく保つ」という指示を守っていたのも印象的でした。task2 の DOM 案は常時読み込む部品を 4,474 バイトまで膨らませていましたが、合成役はそれを採らず、2,249 バイトに抑えています。 結果4:点数 最後に、JobBench の採点基準(ルーブリック)で、元のハーネスの成果物(base1)と、Harness² が採用した成果物(agg1)を採点しました。採点役も同じ Qwen3.8-27B です。JobBench の点数は、採点項目(配点6〜10)ごとに「細かい基準をすべて満たしたら配点を加える」方式で付きます。 手元の実測です。task1 は大きく上がった一方、task2 は下がりました。 課題 base1(元のまま) agg1(採用) 差 細かい基準の合格数 task1(66点満点) 38点(57.6%) 54点(81.8%) +24.2 35/40 → 38/40 task2(60点満点) 22点(36.7%) 12点(20.0%) −16.7 19/32 → 18/32 平均 47.1% 50.9% +3.8 — task1 では、base1 が落とした3項目(dataset.deleted が存在しないイベントであることの説明、レート制限の区分、本番とサンドボックスの URL)を agg1 がすべて取りました。3つめはまさに、INV 役が見つけた URL の問題です。逆に「エンドポイントの版と変更履歴の対応」の1項目だけは agg1 が落としています。 task2 の −16.7 は、配点10の1項目「GET /user/login のレスポンスヘッダーを、型・説明・値の例つきで書いているか」の合否が入れ替わっただけで生じていました。base1 は採点役に「4つのヘッダーすべてに型・説明・例がある」と判定され、agg1 は「型と説明はあるが例の値がない」として、この項目を落としています。細かい基準の数で見ると 19 対 18 でほぼ同じです。逆に agg1 は「よくある移行のつまずき」の項目で基準を1つから3つに増やしていますが、全部は満たせず加点には届きませんでした。 なお、base1 も agg1 も、移行ガイドに curl と Python の変更前後のコード例を書いておらず、配点10の項目を落としています(agg1 にあるコードブロックは URL を示す2つだけでした)。ハーネスを直す側(同じ Qwen)も、この抜けには気づきませんでした。採点役の説明には「コードブロックが1つもない」と事実と少し違う記述もあり、ローカルモデルの採点をうのみにしない方がよいことも分かります。 この結果から言えるのは次の範囲です。 言えること:正解を見ずに、実行の対比から「作った値を書かない」「最後に開き直して照合する」という具体的な直しが生まれ、その直しが効く項目では点が上がった(task1)。その経験は、次の課題に部品ごと受け渡された(task2) 言えないこと:Harness² が手元のモデルで平均的に点を上げるかどうか。各1回の実行で、採点の合否は配点の大きい1項目で大きく振れます。論文も各設定を3回ずつ回して平均しています 論文との違い:編集役が Claude Opus 4.8 ではなく実行役と同じ 27B のローカルモデルで、スキルの読み込みは後述の理由で失敗していました 気づいた注意点 手元の実験では、結果の読み方に関わる問題が3つありました。 1. スキルの読み込みが全部失敗していた(Mac の OpenCode) 実行役がスキルを呼ぶたびに、ripgrep archive did not contain executable というエラーが出ていました。今回の版の OpenCode は、PATH に rg(ripgrep)が無いと GitHub から取得して一時フォルダに展開しますが、その一時フォルダを片付けてから実行ファイルの有無を確かめているため、必ず失敗します。Linux では rg が入っていることが多く、表に出にくい問題だと思われます。 今回の実行では、エージェントはエラーのあと SKILL.md をファイルとして直接読んで手順に従ったり、同じ確認を自分でスクリプトに書いて実行したりしていました。スキルは「呼べなかったが、中身はおおむね使われた」状態で、論文の環境とは条件が違います。次のどちらかで直ります。 方法1:Homebrew で ripgrep を入れる(PATH 上の rg が使われる) brew install ripgrep 方法2:OpenCode が取得済みのアーカイブから rg を取り出し、OpenCode が探す場所に置く cd ~/.cache/opencode/bin tar -xzf ripgrep-14.1.1-aarch64-apple-darwin.tar.gz ripgrep-14.1.1-aarch64-apple-darwin/rg mv ripgrep-14.1.1-aarch64-apple-darwin/rg ./rg && rmdir ripgrep-14.1.1-aarch64-apple-darwin 方法2で rg を置いたあと、簡単なスキル(SKILL.md に合言葉を書いたもの)を OpenCode に読み込ませると、skill ツールが成功し、合言葉を正しく返しました。これから試す方は、実験の前にこの手当てをしておくことをおすすめします。 2. 採点の時間切れは0点として記録される JobBench の採点器は、採点項目ごとの問い合わせを8本同時に送り、240 秒で返らないと3回まで再試行し、それでもだめならその項目を不合格(0点)として記録します。LM Studio は同時に2本しか処理しないので、そのままだと待たされた項目が「本当は合格かもしれないのに0点」になります。最初は2件を同時に採点していたため計16本が LM Studio に並び、1項目しか終わらないまま25分たったのを見て止め、同時数を2、待ち時間を1時間に変えてやり直しました(run_local.py の追加分)。 run_local.py に追加:JobBench の採点器に渡す引数を、ローカル向けに書き換える JUDGE_WORKERS = os.environ.get("LOCAL_JUDGE_WORKERS", "2") JUDGE_TIMEOUT = os.environ.get("LOCAL_JUDGE_TIMEOUT", "3600") _run = jobbench.subprocess.run def _judge_run(cmd, *a, **kw): if isinstance(cmd, list) and any(str(c).endswith("judge.py") for c in cmd): cmd = list(cmd) for flag, value in (("--max-workers", JUDGE_WORKERS), ("--timeout-per-rubric", JUDGE_TIMEOUT)): if flag in cmd: cmd[cmd.index(flag) + 1] = value kw["timeout"] = max(int(kw.get("timeout") or 0), 6 * int(JUDGE_TIMEOUT)) return _run(cmd, *a, **kw) jobbench.subprocess.run = _judge_run 下の分岐の judge のところも、採点を1件ずつにするよう書き換える elif cmd == "judge": from harness2.cli import judge as _judge _judge._PARALLEL = 1 from harness2.cli.judge import main 途中まで書かれた採点ファイルは、次回そのまま「採点済み」として読まれるので、やり直す前に eval_result/ を消しておく必要があります。 3. ネットの禁止は bash で抜けられる OpenCode の webfetch と websearch は禁止しましたが、エージェントは bash の curl で外部にアクセスしていました。課題が名前を挙げている参考資料(GitHub の gist にある API 文書の見本、Microsoft のスタイルガイドなど)を取りに行ったもので、すべての軌跡を調べた範囲では、Hugging Face 上の採点基準(RUBRICS.json)や JobBench のリポジトリへのアクセスはありませんでした。それでも、ツールの設定だけでは通信を止められないことは確かです。公式ガイドのとおり、評価に使うならファイアウォールや DNS で通信先を塞ぐ必要があります。 トラブルシューティング 手元で実際に引っかかった点と、ソースコードを読んで分かった注意点を、確かめる順にまとめます。とくに④と⑦は、エラーで止まらずに結果だけがゆがむので気をつけてください。 左がログに出る文言、右が対処です。④⑦はエラーで止まらず、結果だけが変わります。 症状(ログの文言) 原因 対処 Refusing to overwrite nonempty dependencies/… 準備が途中で止まった残り そのフォルダを別名に移して harness2-setup をやり直す HARNESS2_SOLVER_MODEL is required … モデルの指定がない(.env.example は同梱されていない) lmstudio/qwen/qwen3.8-27b のように「プロバイダー/モデル」で環境変数に書く No such file or directory: 'timeout' macOS に GNU の timeout が無い 代わりのスクリプトを PATH の先頭に置く(または coreutils) TIMEOUT や runner wall-timeout ローカルモデルが遅く、実行役の上限(OpenCode は 2400 秒)を超えた 上限を延ばす(今回は 5400 秒) ripgrep archive did not contain executable OpenCode が ripgrep の自動取得に失敗する brew install ripgrep、または ~/.cache/opencode/bin/rg に置く 編集役が非常に遅く、LM Studio に思考が流れる 編集役は --variant high 付きで呼ばれる effort を空にして --variant を付けない no Vertex access token … judge not run 採点役が gcloud のトークンを取りに行く HARNESS2_JUDGE_BASE_URL と HARNESS2_JUDGE_API_KEY を設定する 採点項目が時間切れで不合格になる 8本同時・240 秒の待ち時間で、時間切れは0点として記録 同時数を減らし、待ち時間を延ばす。途中の eval_result/ は消してからやり直す 自分の仕事に載せるには:アダプターを書く Harness² は、ベンチマークごとの違いを アダプター(adapter) という1つのクラスに閉じ込めています。harness2/core/spec.py の BenchmarkAdapter には「これを実装すれば方式全体が動く。ここに方式のロジックは一切ない」と書かれています。自分の業務課題で試したいときに書くのは、このアダプターです。 主なメソッドを表にまとめます。 メソッド 返すもの 書くときの注意 tasks() 課題 ID の一覧 順番を固定する(ライブラリの状態を再現できるように) domain_of(task_id) 分野の名前 ライブラリはこの単位で分かれる task_prompt(task_id) エージェントに渡す依頼文 採点基準や期待する出力は抜いておく task_files(task_id) 元資料のフォルダ 編集役も context/source/ として読む execute(task_id, harness_dir, run_id) 1回の実行のフォルダ harness_dir=None は元のハーネス。同じ run_id なら結果を使い回す delivered(run_dir) 成果物ができたか 何も出なかった実行を見分ける trajectory(run_dir) 読める形の実行記録 [ACTION]/[OBSERVATION]/[ERROR] の書式にそろえる deliverable_text(run_dir) 成果物の文字列 エージェントが書いたものだけから取り出す base_harness() 初期ハーネスのフォルダ 8つの部品のファイルを置く validate_tree(tree) なし(不備なら例外) 例外のメッセージがそのまま編集役への差し戻し理由になる score(run_dir, task_id) 採点結果 選択が終わった後にしか呼ばれない。失敗時は None(0点と区別する) 付属デモの harness2/demo.py(約160行)が、モデルを使わない最小のアダプターとしてそのまま手本になります。デモでは execute() が Python スクリプトを実行するだけですが、ここを OpenCode や Codex の起動に置き換えれば、自分の課題で同じ流れを回せます。 もう1つ面白い作りがあります。課題の説明文(BenchPrompts)に「benchmark」「evaluation」「grader」「rubric」「score」といった語が入っていると、エラーで止まるようになっているのです。エラーには「測り方ではなく仕事そのものを書け。実務家に必要なことを言え」とあります。アダプターを書く人がうっかり採点の話を指示文に混ぜないための安全装置です。 まとめ Harness² は、AI エージェントのハーネス(指示文・スキル・スクリプトなどの道具立て)を、正解のない仕事の中で AI 自身に直させる枠組みです。要点を振り返ります。 正解の代わりに「行動の違い」を証拠にする:基準の実行、2つの直し案(やり方の INV と分野の知識の DOM)の実行を見比べ、証拠のある変更だけを合成します 直し方の経験を次へ持ち越す:ハーネスそのものは毎回初期状態から始め、採用したハーネス一式と経験メモをライブラリに残して、次の仕事の編集役に読ませます 論文では一貫して改善:1回の改善で3ベンチマーク・4通りの組み合わせすべてが上がり、k を増やすとさらに伸びました。ただし計算量は増えます Mac でも動く:付属デモはそのまま動きました。JobBench は、timeout の代わりのスクリプトなど3か所を補えば、LM Studio のローカルモデルだけで最後まで回せました(スキルを正しく読ませるには ripgrep の手当ても必要) 手元でも経験の持ち越しを確認:task1 で見つかった「ファイル間で値をそろえる」決まりが、ライブラリ経由で task2 のハーネスに取り込まれました 点数は上下した:task1 は 57.6% → 81.8%、task2 は 36.7% → 20.0%。各1回・採点役も同じ 27B モデルなので、点数の良し悪しは決められません 一方で、採点基準のネット経由の漏れ、実行のたびのゆらぎ、編集役と実行役が同じ思い込みを持つ危うさなど、使う側が気をつけるべき点もはっきりしています。 次の一歩としては、まず harness2-demo で流れをつかみ、付属デモのアダプター(harness2/demo.py)をまねて、自分の小さな業務課題を1つ載せてみるのがおすすめです。採点の仕組みが自分で用意できる課題(決まった形式のレポート、表の集計など)から始めると、改善の効き目を確かめやすくなります。 参考リソース GitHub:google-research/harness2(Apache 2.0) 論文:Ruiyao Xu ほか「Harness²: Recursive Agent Harnessing for an Open World」、arXiv 2610.14224(2026年10月9日)arxiv.org プロジェクトページ:harness2.github.io リポジトリ内のガイド:docs/SETUP.md(準備)、docs/EXPERIMENTS.md(実験の進め方とネットの制限)、docs/BENCHMARKS.md(各ベンチマーク) JobBench:Job-Bench/job-bench-eval OpenCode:anomalyco/opencode(Harness² がピン留めしている版の取得元)
Google の Harness² を Mac で動かしてみた !Google の Harness² を Mac で動かしてみた はじめに Claude Code や Codex のような AI エージェントを仕事で使っていると、同じモデルなのに「指示文を1行足しただけで急にうまくいく」「スキルを1つ作ったら失敗が消えた」という経験をよくします。モデルの性能だけでなく、モデルの周りに置く道具立てが結果を大きく左右するからです。 この道具立てを、最近は ハーネス(harness) と呼びます。システムプロンプト、守るべき決まり、スキル、補助スクリプト、サブエージェントなど、モデルを「仕事ができるエージェント」にするための仕組み全体のことです。 ただ、ハーネスの調整はたいてい人の手仕事です。失敗した実行ログを読み、勘で指示文を書き足し、もう一度試す。しかも新しい種類の仕事には 正解も採点基準もない ので、直した結果が本当に良くなったのか確かめにくいという問題があります。 2026年10月9日に Google Cloud AI Research などのチームが論文を公開した Harness²(ハーネス・スクエア) は、この調整を AI 自身に任せる枠組みです。コードも GitHub の google-research/harness2 で Apache 2.0 として公開されています。 まずは、何が変わるのかを図で見てください。左が人の手で直す従来の流れ、右が Harness² の流れです。 !左は人が実行ログを読み勘で指示文を足す従来の流れ、右はHarness²が今のハーネスで一度実行し、直し案を2つ作って同じ仕事で試し、実行の違いを根拠に合成し、経験メモを次に持ち越す流れ 正解がなくても「直す前と後で実行がどう変わったか」は観察できます。Harness² はこの違いを証拠にして直し、直し方の経験まで次の仕事に持ち越します。 この記事では、Harness² の仕組みを初めての人にも分かるように順を追って説明し、論文の結果を整理したうえで、実際に手元の Mac(M5 Max)で動かした結果 を紹介します。公式の手順は Google Cloud(Vertex AI)の Gemini を前提にしていますが、今回は LM Studio のローカルモデル(Qwen3.8-27B)だけ で、実在のベンチマーク JobBench の課題を回してみました。 先に手元の結果を書いておくと、2問のうち1問目は元のハーネスの 57.6% から 81.8% に上がり、2問目は 36.7% から 20.0% に下がりました。点数の上下より印象的だったのは、AI が1問目で見つけた弱点(文書ごとに URL を勝手に作る)への直し方が、ライブラリを通じて2問目のハーネスにそのまま受け継がれたことです。詳しくは後半で紹介します。 この記事で分かること - ハーネスの8つの部品と、Harness² がどこを書き換えるのか - 正解なしで直すための「Propose → Probe → Compose」という1回の改善ステップ - 経験を次の仕事に持ち越す「ライブラリ」の中身 - 論文の結果と、その読み方の注意点 - Mac で付属デモを動かす手順と、ローカルモデルだけで JobBench を回す方法・つまずきどころ 想定読者 AI エージェント(Claude Code、Codex、OpenCode など)を使ったことがあり、プロンプトやスキルを自分で調整した経験がある方を想定しています。Python の仮想環境を作ってコマンドを実行できれば、手元での再現も追えます。 この記事の確認範囲 - 確認日:2026年10月21〜22日 - リポジトリ:google-research/harness2 のコミット 1cfe254(2026年10月13日「Fix arXiv link」)、パッケージ版 0.1.0 - 論文:arXiv 2610.14224 v1(2026年10月9日投稿) - 検証環境:MacBook Pro(Apple M5 Max、メモリ 128GB)、macOS 26.6.2、Python 3.12.15、uv 0.12.22、LM Studio(Qwen3.8-27B 4bit) - 論文の数値は論文の表から引用しています。手元で測った数値は「手元の実測」と明記して区別します ハーネスとは何か:8つの部品 Harness² の論文は、エージェントを「モデル+ハーネス」と捉えます。モデルの重み(学習済みのパラメータ)は一切変えず、周りのハーネスだけを書き換えて性能を上げる、という立場です。 Harness² が書き換えの対象にする部品は8つあります。どれも1つのファイルとして置かれるので、直した内容は「差分」として残り、あとから人が読んで確かめられます。 !Harness²が書き換える8つの部品。文脈づくりのシステムプロンプト・ガードレール・メモリ・スキル、道具の使い方のツールの手引き・スクリプト、仕事の分担のサブエージェント、ツール呼び出しへの割り込みのプラグイン。中央に固定のモデル 常時読み込まれる部品と、呼んだときだけ働く部品があります。Harness² の編集役は「常時の文章は毎回コストがかかる」と指示されています。 8つの部品と、実行時に効くタイミングを表にまとめます。 | 部品 | ファイル | 効くタイミング | 役割 | |---|---|---|---| | C1 システムプロンプト | systemprompt.md | 毎回 | 役割と全体の手順 | | C2 ガードレール | guardrails.md | 毎回 | どんな仕事でも守る決まり | | C3 メモリ | LongTermMEMORY.md | 毎回 | その分野の不変の事実 | | C4 ツールの手引き | tool_descriptions/tool_guidance.md | 毎回 | 既存ツールをいつ・どう使うか | | C5 サブエージェント | sub_agents/<名前>/AGENT.md | task ツールで呼んだとき | 別の文脈で調べ、結果だけ返す | | C6 スキル | skills/<名前>/SKILL.md | skill ツールで呼んだとき | 専門の手順を必要なときだけ読む | | C7 スクリプト | scripts/<名前>.py | bash で実行したとき | 決まった処理を確実にこなす | | C8 プラグイン | plugin/<名前>.ts | ツール呼び出しのたび | モデルが飛ばせない処理を差し込む | この表で大事なのは「いつ効くか」の列です。常時読み込まれる部品に文章を足すと、毎回の実行でモデルの文脈(コンテキスト)を圧迫します。リポジトリの編集役向けの指示(harness2/core/prompts.py)には、新しい手順はまず スキル、決まった処理は スクリプト、入力が大きく答えが小さい調べものは サブエージェント にする、という優先順位まで書かれています。 Claude Code を使っている方なら、CLAUDE.md・スキル・フック・サブエージェントを思い浮かべると近いイメージです。Harness² は、そうした部品を AI が自分で書き足したり書き換えたりする仕組みだと考えてください。 Harness² の全体像:2段の「再帰」 Harness² の名前の「²(2乗)」は、改善の輪が2段になっていることを表しています。 1つめの輪は タスク内の再帰(task-level recursion) です。今取り組んでいる仕事のために、ハーネスを試して直します。2つめの輪は ハーネス改善の再帰(harnessing-level recursion) です。直した経験を記録し、次の仕事で「どう直すか」を良くします。 !2段の再帰。上の輪は仕事tに対してPropose・Probe・Composeでハーネスを直し成果物を提出する。下の輪は振り返りで何が効いたかを書き、ライブラリに残す。次の仕事も初期ハーネスH0から始めるが、直すときにライブラリを読む モデルの重みも、各段階への指示文も固定です。変わるのは「この仕事のハーネス」と「編集役が読むライブラリ」の2つだけです。 ここで押さえておきたいのは、次の仕事のハーネスは毎回まっさらな初期状態(H0)から始まる という点です。前の仕事で直したハーネスをそのまま引き継ぐわけではありません。 論文は、前の仕事に合わせ込んだハーネスをそのまま使い回すと、仕事の種類が変わったときに逆効果になりやすいと指摘しています。そこで Harness² は、ハーネスそのものではなく「直し方の経験」を引き継ぎます。編集役が新しい仕事のハーネスを直すとき、ライブラリにある過去のハーネスと経験メモを参考として読み、使える部品だけを選んで取り込む、という形です。 登場する役割を整理しておきます。 | 役割 | 論文での呼び名 | やること | |---|---|---| | 実行役 | Agent Executor | 固定のモデルが、ハーネスを使って実際に仕事をこなす(OpenCode か Codex の上で動く) | | 編集役 | Harness Editor | 実行の記録を読み、ハーネスの直し案を作る | | 合成役 | Harness Composer | 直し案とその実行結果を比べ、最終的なハーネスを作る | | 選択役 | Selector | k>1 のとき、作り手を隠した成果物の中から1つ選ぶ | | 振り返り役 | Experience Reflector | 改善の記録全体を読み、ライブラリに経験メモを書く | 実行役以外の4つは、どれも同じ「改善用のモデル」が担当します。論文の主な実験では Claude Opus 4.8 が OpenCode 上でこの役を務めました。リポジトリでは HARNESS2_IMPROVER_MODEL で指定でき、実行役と同じモデルにすることもできます。 正解なしで直す仕組み:Propose → Probe → Compose Harness² の中心は、1回の改善ステップ「Propose(提案)→ Probe(試行)→ Compose(合成)」です。正解がない状況で、どうやって「この変更は効いた」と判断するのかを見ていきましょう。 !1回の改善ステップ。base1を今のハーネスで実行し、INV案とDOM案を作り、それぞれ同じ条件で実行して、base1との行動の違いを確かめ、合成役agg1が合成・1案採用・KEEP_BASEのどれかを選んで実行し提出する k=1 の場合、1つの仕事につき実行は4回、編集役のセッションは3回です。採点基準や点数は、どの段階でも編集役に見せません。 1. 基準になる実行をする まず、今のハーネスのまま1回仕事をさせます(base1)。この実行で残るのは、エージェントが何をしたかの記録(論文では「軌跡」、trajectory)と成果物です。 ここで重要なのは、この実行が 正しかったかどうかは分からない という点です。論文も「基準の実行はハーネスがどう振る舞うかを示すだけで、成果物が正しいかは示さない」と書いています。 2. 2種類の直し案を作る(Propose) 編集役は、基準の実行を読んで直し案を2つ作ります。担当範囲をあえて分けているのが工夫です。 - INV 案(invariant、分野を問わないやり方):計画の立て方、ツールの使い方、出力の確認漏れなど、どの分野でも起きる弱点を直します。プロンプトでは「専門知識には触れるな」と指示されます - DOM 案(domain、分野の知識):その職種の人なら知っている手順、成果物の型、確認項目を、スキルなどの形で書きます。プロンプトでは「コマンドやリトライの話はするな」と指示されます どちらの案も、ハーネスのファイルを書き換えた「すぐ実行できるハーネス一式」として出てきます。 3. 同じ条件で試す(Probe) 2つの案を、同じ仕事・同じモデル・同じ環境 でもう一度ずつ実行します。こうすると、基準の実行と見比べられます。 たとえば「基準の実行では出力ファイルを書いたあと開き直していなかったが、INV 案の実行では検証スクリプトで開き直し、欠けていた節に気づいて書き足した」という違いが見えれば、それがその変更を残す根拠になります。論文はこれを 行動の対比(behavioral contrast) と呼んでいます。 4. 証拠のある変更だけを組み合わせる(Compose) 最後に合成役が、基準の実行、2つの案とその差分、それぞれの実行記録をすべて読み、最終的なハーネスを作ります。選べるのは次の3つです。 - 両案から効いた変更を 合成 する(INV と DOM がそれぞれ根拠を持つときに期待される形) - 1つの案を そのまま採用 する - KEEP_BASE:何も直さない リポジトリのプロンプトには「KEEP_BASE は失敗ではなく正当な答え。証拠のない変更は退行(悪化)だ」とはっきり書かれています。さらに、判断のたびに「どの実行のどの場面が根拠か」を1行ずつ書くよう求めます。点数が上がりそうだから、ではなく、観察した事実に結びつけて判断させるわけです。 合成したハーネスでもう一度実行し、その成果物を提出します。k=1 ではこの成果物がそのまま採用されます。 「正解なし」でも成り立つ理由と限界 この方式は、成果物の正しさを判定するのではなく、ハーネスの変更が行動をどう変えたか を見ています。「確認スクリプトを入れたら、実際に開き直すようになった」は正解がなくても観察できます。 一方で論文は、この証拠には限界があるとも明言しています。実行には毎回ゆらぎがあり、編集役と実行役が同じ思い込みを持っていれば気づけません。合成役への指示にも「1回の実行で良かった・悪かったは弱い証拠」「基準の実行で繰り返し起きた失敗こそ強い証拠」と書かれています。 k を増やす:並列と逐次 1回の改善ステップの「試す回数」を増やす方法が2つ用意されています。リポジトリでは --k と --mode で切り替えます。 !k=2の例。並列は同じ証拠から候補を4つ作り、合成役2人が独立に合成して最後に1つ選ぶ。逐次は1周目の合成c1と証拠を持ち越して2周目を行い、c1とc2から1つ選ぶ どちらのモードも、1つの仕事あたり実行 4k 回、編集役のセッション 3k 回です。k>1 のときは最後に成果物を1つ選ぶ段階が加わります。 - 並列(parallel):基準の実行を k 回とり、INV 案と DOM 案を k 個ずつ作って試します。その全部を材料に、k 人の合成役が互いを見ずに合成します。横に広く探る方法です - 逐次(sequential):1周目で合成したハーネスと証拠を持ち越して、2周目の Propose → Probe → Compose を行います。縦に深く磨く方法です k>1 のときは、最後に選択役が k 個の最終候補(並列なら agg1〜aggk、逐次なら c1〜ck)の成果物を比べて1つ選びます。このとき選択役には、どの成果物がどのハーネスから出たかを伏せ、番号もばらばらに並べます。プロンプトには「どの候補も特別扱いしない。手の込んだものを選んでも加点はない」と書かれています。見た目が豪華な成果物に引っ張られる偏りを避けるためです。 論文の WorkBuddy-Bench(Code と Office)の実験では、k を 1 から 5 に増やすと両モードとも単調に点が上がりました。k=5 では並列が逐次を上回り、Code は 76.5→80.8、Office は 79.5→83.4 になっています(並列)。ただし k を増やすほど実行回数とモデルの利用料も比例して増える点には注意が必要です。 ライブラリ:直し方の経験を次の仕事へ 仕事を1つ終えるたびに、振り返り役が改善の記録全体(基準の実行、各案、合成の判断、最終の成果物)を読み、分野ごとのライブラリに書き込みます。中身を、付属デモを実行して実際に書き出されたファイルで見てみましょう。 !ライブラリのフォルダ構成と、デモで実際に書かれたEXPERIENCE.mdの中身。各変更が何をしたかの行と、次に持ち越す教訓が1つ。右側は次の仕事での使われ方 経験メモには「各変更が何をして、実行がどう変わったか」と「次に持ち越す教訓(最大1つ)」が残ります。根拠が弱ければ教訓は書きません。 ライブラリの1件は、次の2つで構成されます。 - 採用したハーネス一式:要約した文章ではなく、スキルやスクリプトのファイルそのものを残します - EXPERIENCE.md:なぜこのハーネスが選ばれたか、各変更の判定、次に持ち越す教訓 各変更の判定は helped(効いた)、hurt(悪化させた)、no observed effect(効果なし)、confounded(切り分けられない)の4種類です。振り返り役への指示には「複数の部品を同時に変えて、どれが効いたか切り分けられないときは confounded を選べ」「でっち上げた因果は、後のすべての仕事のライブラリを汚す」とあります。確かな対比が見つからなければ、教訓の代わりに NO_ENTRY と1行だけ書きます。 次の仕事では、このライブラリが編集役の作業フォルダに context/library/ として置かれます。編集役は目次(INDEX.md)を見て関係しそうなものを開き、使いたい部品があれば自分のハーネスへコピーします。そのとき、前の仕事で分かった事実(ファイル同士の対応表など)は「この仕事では仮説にすぎない」ので、目の前の証拠で確かめ直すよう指示されています。 論文の結果を読む 論文は、3つの業務系ベンチマークで評価しています。 | ベンチマーク | 内容 | 指標 | |---|---|---| | WorkBuddy-Bench(Tencent) | 事務・Web・コードの成果物づくり(Code/Web/Office の3分野) | 採点役のスコア(3分野の平均) | | JobBench | 職種ごとの実務課題。長い実行が必要 | 採点基準(ルーブリック)による重み付きスコア | | LAB(Harvey) | 法律業務 | 採点項目ごとの合格率(Partial) | 実行役は Gemini 3.5 Flash と GLM-5 の2種類、土台のハーネスは OpenCode と Codex の2種類で、組み合わせは4通りです。それぞれ3回ずつ、仕事の順番を入れ替えて実行した平均が報告されています。 !論文の結果。OpenCodeでの元のハーネス、Harness² k=1、並列の再帰k=5のスコア。Gemini 3.5 FlashはWorkBuddy 72.4→77.5→80.8、JobBench 31.5→36.0→39.0、LAB 79.9→81.9→84.5。GLM-5はWorkBuddy 64.2→75.7→79.6、JobBench 20.6→24.2→28.4、LAB 59.6→71.4→78.1 OpenCode 上の結果です。1回の改善(k=1)で全項目が上がり、並列の再帰(k=5)でさらに上がっています。 主な結果を、私なりに整理すると次のとおりです。 - 1回の改善だけで、4通りの組み合わせすべて・3ベンチマークすべてで元のハーネスを上回りました。最大の伸びは WorkBuddy-Bench が +11.5(GLM-5、OpenCode)、JobBench が +4.5(Gemini 3.5 Flash、OpenCode)、LAB が +11.8 ポイント(GLM-5、OpenCode)です - 弱いモデルほど伸びが大きい傾向があります。WorkBuddy-Bench では、改善後の GLM-5(75.7)が元のハーネスの Gemini 3.5 Flash(72.4)を上回りました - 並列の再帰(k=5)を足すと、さらに WorkBuddy-Bench で +2.9〜3.9、JobBench で +3.0〜4.2、LAB で +2.6〜6.7 ポイント上がりました - 比較手法との関係では、WorkBuddy-Bench の平均と JobBench のスコアは4通りすべてで Harness² が最高でした。LAB の Gemini 3.5 Flash × Codex だけは、事前に最適化する Meta-Harness が上回っています 何が効いているのか:部品を外す実験 論文は、Harness² の3つの工夫を1つずつ外す実験(アブレーション)も行っています。Gemini 3.5 Flash × OpenCode、k=1 での結果です。 | 条件 | WorkBuddy-Bench | JobBench | |---|---|---| | Harness²(すべてあり) | 77.5 | 36.0 | | ライブラリなし | 74.4(−3.1) | 34.2(−1.8) | | INV/DOM の役割分担なし(汎用の編集役2人) | 74.6(−2.9) | 32.3(−3.7) | | 合成なし(良さそうな方を1つ採用) | 75.9(−1.6) | 34.1(−1.9) | どれを外しても点が下がっており、3つとも効いていると読めます。特に JobBench では役割分担を外したときの落ち込みが大きく、「やり方」と「分野の知識」を分けて考えさせる設計の意味が見えます。 試さずに直すと悪化する もう1つ興味深いのは、実行して確かめずにハーネスを直すと、かえって悪くなる という結果です。WorkBuddy-Bench の Code では、基準の実行も試行もライブラリもなしで直接ハーネスを書き換えると、73.4 から 63.2 に下がりました。Web も 69.9 から 65.0 に下がっています。 一方、k=5 で育てたライブラリだけを持たせると、追加の試行なしでも Code・Web・Office がそれぞれ +0.9、+4.4、+5.1 上がりました。さらに JobBench で育てたライブラリを WorkBuddy-Bench に持ち込んでも、+0.6、+0.5、+2.9 の改善が見られています。 編集役が強いモデルでなくてもよい 「編集役に強いモデルを使っているから伸びるのでは」という疑問に対しては、実行役と編集役を同じモデルにした実験があります(OpenCode、k=2)。GLM-5 から Claude Opus 5 までの5モデルすべてで、3つのベンチマーク全部が改善しました。GLM-5 自身が編集役を務めた場合でも LAB で +13.7 です。 読むときの注意点 論文の数字を自分の仕事に当てはめる前に、次の点を押さえておくとよいでしょう。 - 追加の計算が必要:k=1 でも1つの仕事に実行4回・編集役のセッション3回(+振り返り)がかかります。論文の計算量の比較では、Harness² は「同じ計算量なら他の手法より高い点」の境界線(パレート前線)上にありますが、元のハーネスより安くなるわけではありません - 採点はモデルによる評価:JobBench と WorkBuddy-Bench の点数は、採点役のモデルが付けたものです - 失敗例もある:論文の失敗分析では、選択役が 95 点の成果物ではなく 55 点の成果物を選んだ例や、間違った前提が全候補に広がった例が挙げられています - 本番のハーネスとの比較:WorkBuddy-Bench では、元の Claude Code・Codex・OpenCode などを使った中で最も良いものと比べても、OpenCode から始めた Harness²(k=5)が各分野で 4.6〜5.8 上回りました。ただし、これは追加の計算を使った上での比較です 答えを見せない・壊れた案を通さないための仕掛け 正解なしで直す仕組みは、裏を返すと「採点基準が少しでも混ざれば、それに合わせ込む試験対策になってしまう」危うさがあります。リポジトリには、そのための対策がいくつも入っています。 !編集役に答えを見せず壊れた案を通さないための6つの仕掛け。採点の情報を渡さない、証拠はデータ扱い、形の検査と1回だけの再挑戦、匿名で選ぶ、証拠の弱い教訓は残さない、残る穴としてネット経由の漏れ ①〜⑤はリポジトリのコードとプロンプトに入っている対策です。⑥は利用者が自分で塞ぐ必要があります。 1つずつ補足します。 1. 採点の情報を渡さない:編集役に渡す作業フォルダに RUBRICS.json などの採点基準や、採点結果の痕跡が入っていたら、受け渡しの前に止めます(harness2/core/context.py)。指示文にも「採点基準は存在しない。採点者が何を求めているか推測するな」と書かれています 2. 証拠はデータ扱い:課題ファイルや候補ハーネスの中に命令文が書かれていても従わないよう指示します。候補に入ったスクリプトは実行せず、読んで判断します。プロンプトインジェクション(データに紛れ込ませた命令で AI を操る攻撃)への備えです 3. 形の検査と1回だけの再挑戦:呼び出し方が書かれていないスキル、コンパイルできないスクリプト、スキルが使う補助ファイルの同梱漏れなどは不合格にします。理由を添えて1回だけやり直させ、それでもだめなら元のハーネスのまま進めます 4. 匿名で選ぶ:k>1 の最終選択では、成果物の作り手を伏せ、順番も入れ替えて比べます。選択役は既存の成果物から選ぶだけで、新しく書き直すことはしません 5. 証拠の弱い教訓は残さない:切り分けられない差は confounded、確かな対比がなければ NO_ENTRY です そして ⑥ が、利用者が自分で手当てしなければならない穴です。リポジトリの実験ガイドには、実行役のネット接続は制限されていない こと、JobBench は各課題の採点基準を Hugging Face で公開していること、公式の検証中に Codex が指示されてもいないのに採点基準をダウンロードしたことが書かれています。評価に使う前に、実行役のマシンから huggingface.co や github.com への通信を、ファイアウォールや DNS の設定で塞ぐよう勧めています。 Mac で動かしてみる(1):インストールと付属デモ ここからは、実際に手元の Mac で動かした手順と結果です。公式の README は「Linux、Python 3.12、uv」を前提にしていますが、パッケージの準備と付属デモは macOS でもそのまま動きました。 準備するもの | もの | 手元で使った版 | 用途 | |---|---|---| | Python | 3.12.15(Homebrew) | Harness² 本体(3.12〜3.13 に対応) | | uv | 0.12.22 | 仮想環境とパッケージの導入 | | Node.js | 22.23.2 | ベンチマークの準備で npm を使う(20 以上) | | Git | macOS 標準 | 依存リポジトリの取得 | | LM Studio | Qwen3.8-27B(4bit) | 後半のローカル実験で使用 | 付属デモだけなら Python と uv があれば十分です。モデルの認証情報も要りません。 手順1:取得してインストールする # リポジトリを取得する git clone https://github.com/google-research/harness2 cd harness2 # Python 3.12 の仮想環境を作って有効化する uv venv --python 3.12 .venv source .venv/bin/activate # 本体と、成果物の描画に使う追加パッケージ(openpyxl・python-pptx)を入れる uv pip install -e '.[render]' pyproject.toml を見ると、本体の必須依存は空(dependencies = [])で、Python の標準ライブラリだけで動く作りになっています。 手順2:付属デモを動かす # 2つの小さな会計タスクを、並列・逐次の両方の方式で k=2 で回す harness2-demo --output runs/demo --k 2 このデモは、モデルを呼ばない「台本どおりの編集役」と、本物の Python 実行を組み合わせて、Harness² の流れ全体(実行 → 直し案 → 試行 → 合成 → 選択 → 振り返り → ライブラリ)を通します。手元の実行結果がこちらです。 !ターミナルでharness2-demoを実行した画面。2タスクとも ok、実行時間3.1秒、summarize.py の差分で正の数だけ足す条件が消えたこと、実行28回・編集役の呼び出し32回が表示されている 手元の実測:2タスクとも完了、3.1 秒。直し案によって scripts/summarize.py の「正の数だけ足す」条件が外れ、出金を含めた正しい合計になりました。 デモの課題は「台帳の金額を符号つきで合計せよ」です。ところが初期ハーネスに入っている scripts/summarize.py には、正の数だけを足すバグが仕込まれています。そのため、基準の実行は台帳1で Total: 105.25 を出しますが、正しくは出金の −20.10 を含めた Total: 85.15 です。 編集役(デモでは台本)がこの条件を外すと、試行の実行で合計が変わります。選択役は元の台帳と照らして正しい合計の成果物を選び、振り返り役は「入金と出金が混ざった台帳では符号を両方含める」という教訓をライブラリに残しました。前の節の図6で見た EXPERIENCE.md は、このとき実際に書き出されたものです。 数字も確かめておきます。1タスクあたり、並列(k=2)が実行8回、逐次(k=2)が基準の実行を共有して追加6回なので、2タスクで28回です。編集役の呼び出しは、1タスクあたり両方式の改善段階が 3k×2=12回、選択が2回、振り返りが2回で16回、2タスクで32回になり、表示と一致します。 2つめのタスク(ledger-2)の編集役の作業フォルダを開くと、context/library/ に1つめのタスク(ledger-1)の記録が入っていました。前の仕事の経験が、次の仕事の材料として渡っていることが確認できます。 リポジトリ付属のテストも通りました。 # 付属テストを実行する(pytest を入れてから) uv pip install pytest python -m pytest -q tests # 手元の結果: 5 passed, 2 subtests passed in 13.75s Mac で動かしてみる(2):ローカルモデルだけで JobBench を回す 付属デモは仕組みの確認には十分ですが、編集役が台本なので「AI が本当にハーネスを直せるのか」は分かりません。そこで、実在のベンチマーク JobBench の課題を、モデルを使う役をすべて手元の LM Studio に向けて 回してみました。 公式の手順は Google Cloud の Vertex AI(Gemini や Claude)を前提にしていますが、ソースを読むと、実行役と編集役は OpenCode の「プロバイダー/モデル名」で指定でき、JobBench の採点役は OpenAI 互換の API を指定できる作りでした。そこで、3つの役をすべて1台の LM Studio に向けました。 !3つの役を1台のLM Studioで動かす構成。実行役と編集役はOpenCode経由、採点役はOpenAI互換APIでLM StudioのQwen3.8-27Bを呼ぶ。つなぎの設定opencode-lmstudio.jsonと、Mac向けの補いとしてtimeoutの代わりのスクリプト、時間切れの延長、--variant highの除去 手元の構成です。論文の主な実験(編集役は Claude Opus 4.8、実行役は Gemini 3.5 Flash など)とは、モデルの強さがまったく違う点に注意してください。 実験の条件 | 項目 | 今回の設定 | |---|---| | 課題 | JobBench の職種「technical_writers」の task1・task2(2問を順番に) | | task1 の内容 | API の CSV・開発者メモ・サポート問い合わせから、移行ガイド・API リファレンス・トラブル対応ガイド・リリースノートの4文書を書く | | task2 の内容 | 変更履歴・利用統計・問い合わせから、移行ガイド・改訂版 API リファレンス・影響分析の CSV を作る | | 方式 | 並列、k=1(1問につき実行4回・編集役3回+振り返り1回) | | モデル | 実行役・編集役・採点役すべて Qwen3.8-27B(4bit、LM Studio、思考なし。採点役だけ JobBench 既定の思考 low) | | ネット | OpenCode の webfetch・websearch は禁止。ただし後述のとおり bash の curl は通る | 職種を1つに絞って2問にしたのは、ライブラリの働き(task1 の経験が task2 で使われるか)を見るためです。 手順1:ベンチマークを準備する # リポジトリのフォルダで、仮想環境を有効にした状態で実行する # OpenCode(ピン留め版)・bun・JobBench の採点器と課題データを dependencies/ に入れる harness2-setup jb # 設定の確認(モデルはまだ呼ばない) HARNESS2_SOLVER_MODEL=lmstudio/qwen/qwen3.8-27b harness2-check --bench jb harness2-setup jb は macOS でも最後まで通りました。GitHub から OpenCode と JobBench の採点器を取得し、Hugging Face から課題データ(566ファイル)を落とします。手元では dependencies/ が合計 3.2GB ほどになりました(OpenCode が 2.5GB)。 なお、README には cp .env.example .env とありますが、確認したコミットのリポジトリには .env.example が入っていませんでした。設定は環境変数で直接渡します。 手順2:LM Studio を OpenCode につなぐ LM Studio でモデルを読み込み、サーバーを起動します。 # LM Studio のサーバーを起動し、Qwen3.8-27B を2本並列で読み込む lms server start lms load qwen/qwen3.8-27b --parallel 2 -y 次に、OpenCode に LM Studio をプロバイダーとして登録する設定ファイルを作ります。Harness² は OpenCode を自前の環境で起動しますが、環境変数 OPENCODE_CONFIG で指定したファイルは読み込まれるので、ここに書きます。 { "$schema": "https://opencode.ai/config.json", "provider": { "lmstudio": { "npm": "@ai-sdk/openai-compatible", "name": "LM Studio (local)", "options": { "baseURL": "http://127.0.0.1:1234/v1", "apiKey": "lm-studio" }, "models": { "qwen/qwen3.8-27b": { "name": "Qwen3.8 27B (LM Studio)", "limit": { "context": 131072, "output": 32768 }, "options": { "reasoningEffort": "none" } } } } }, "permission": { "webfetch": "deny", "websearch": "deny" } } reasoningEffort: none で Qwen の思考を切っています。思考ありだと手元では1ターンが数倍遅くなるためです。permission は、採点基準をネットから読みに行くのを防ぐ目的で入れました(ただし後で書くとおり完全には防げません)。 手順3:Mac とローカルモデル向けに3か所を補う そのままでは Mac で動かない箇所や、ローカルモデルに合わない箇所が3つありました。 1. timeout コマンドが無い:JobBench の実行役ランナーは GNU の timeout で OpenCode を起動します。macOS には無いので、No such file or directory: 'timeout' で実行役が1回も動かずに終わります 2. 時間切れが短い:OpenCode の実行役は 2400 秒で打ち切られます。ローカルの 27B モデルでは足りない可能性があります 3. 編集役で思考が有効になる:編集役は OpenCode に --variant high を付けて呼ばれます。手元で試すと、これで設定の reasoningEffort: none が上書きされ、思考が有効になりました 1つめは、timeout と同じ使い方ができる小さな Python スクリプトを作り、PATH の先頭に置いて解決しました(Homebrew の coreutils を入れて timeout という名前で呼べるようにしても構いません)。 #!/usr/bin/env python3 """macOS 用の簡易 timeout:timeout [--kill-after=Ns] 秒数 コマンド...""" import os, signal, subprocess, sys def secs(s): s = s.strip() mult = {"s": 1, "m": 60, "h": 3600}.get(s[-1]) return float(s[:-1]) * mult if mult else float(s) args = sys.argv[1:] kill_after = None while args and args[0].startswith("--"): a = args.pop(0) if a.startswith("--kill-after="): kill_after = secs(a.split("=", 1)[1]) duration, cmd = secs(args[0]), args[1:] p = subprocess.Popen(cmd, start_new_session=True) try: sys.exit(p.wait(timeout=duration)) except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGTERM) try: p.wait(timeout=kill_after or 30) except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGKILL) sys.exit(124) 2つめと3つめは、リポジトリのコードを書き換えずに済むよう、環境変数を入れてから Harness² の CLI を呼ぶ小さなラッパー run_local.py で差し替えました。 """Harness² のすべてのモデル役を LM Studio に向けて動かす(Google Cloud 不要)""" import os import sys from pathlib import Path LAB = Path(__file__).resolve().parent MODEL = os.environ.get("LOCAL_MODEL", "qwen/qwen3.8-27b") os.environ.setdefault("HARNESS2_SOLVER_MODEL", f"lmstudio/{MODEL}") # 実行役 os.environ.setdefault("HARNESS2_IMPROVER_MODEL", f"lmstudio/{MODEL}") # 編集役など os.environ.setdefault("HARNESS2_JUDGE_MODEL", MODEL) # 採点役 os.environ.setdefault("HARNESS2_JUDGE_BASE_URL", "http://127.0.0.1:1234/v1") os.environ.setdefault("HARNESS2_JUDGE_API_KEY", "lm-studio") # 指定すると gcloud を使わない os.environ.setdefault("OPENCODE_CONFIG", str(LAB / "opencode-lmstudio.json")) os.environ["PATH"] = f"{LAB / 'bin'}:{os.environ['PATH']}" # bin/timeout を先に見つけさせる from harness2.benchmarks import jobbench # noqa: E402(設定は環境変数を読んでから) from harness2.core.spec import resolve_channel_model # noqa: E402 ROLLOUT_S = int(os.environ.get("LOCAL_ROLLOUT_TIMEOUT", "5400")) IMPROVER_S = int(os.environ.get("LOCAL_IMPROVER_TIMEOUT", "5400")) # 実行役の時間切れを延ばし、編集役は --variant を付けずに呼ぶ(思考なしのまま) jobbench._DEFAULT_TIMEOUT = {"oc": ROLLOUT_S, "cx": ROLLOUT_S} jobbench._channels = lambda: dict.fromkeys( ("proposer", "selector"), resolve_channel_model(None, None, effort="", timeout=IMPROVER_S), ) cmd, argv = sys.argv[1], sys.argv[2:] if cmd == "sweep": from harness2.cli.run_sweep import main elif cmd == "judge": from harness2.cli.judge import main elif cmd == "report": from harness2.cli.report import main elif cmd == "check": from harness2.cli.check import main else: raise SystemExit(f"unknown command {cmd!r}") raise SystemExit(main(argv)) opencode-lmstudio.json・run_local.py・bin/timeout を local/ というフォルダにまとめ、リポジトリのフォルダから次のように実行しました。 # technical_writers の2問を、並列・k=1 で順番に解かせる(約9時間かかった) export HARNESS2_RUNS_DIR=runs/jb-local-k1 nohup python local/run_local.py sweep --bench jb --k 1 --mode parallel \ --only-domains technical_writers --max-tasks 2 > jb-sweep.log 2>&1 & # 終わったら、採用した成果物(agg1)と元のハーネスの成果物(base1)を採点する python local/run_local.py judge --bench jb --k 1 --mode parallel python local/run_local.py judge --bench jb --k 1 --mode parallel --bases python local/run_local.py report --bench jb --k 1 --mode parallel 長時間の実行なので、nohup で端末から切り離しています。途中で止まっても、同じコマンドをもう一度実行すれば終わった段階は飛ばして再開します。 結果1:かかった時間は2問で約9時間 2問とも最後まで完了し、失敗はありませんでした。各段階の時間は次のとおりです(手元の実測。直し案と試行は2本を並行で実行)。 | 段階 | task1 | task2 | |---|---|---| | 基準の実行(base1) | 27分 | 33分 | | 直し案づくり(INV・DOM の2セッション並行) | 53分 | 35分 | | 直し案の試行(inv1・dom1 の2本並行) | 90分 | 90分 | | 合成(agg1 の編集) | 52分 | 42分 | | 合成したハーネスでの実行(agg1) | 48分 | 37分 | | 振り返り(ライブラリへの記録) | 18分 | 26分 | | 合計 | 4時間46分 | 4時間23分 | 1回の改善(k=1)でも、元のハーネスで1回解くのに比べて8〜11倍の時間がかかっています。ローカルの 27B モデルで2本並行にすると、試行の段階はとくに遅くなりました。費用は電気代だけですが、時間の面では「手元で気軽に回す」ものではない、というのが実感です。 結果2:AI が見つけた弱点と、書いた直し 一番分かりやすかったのは task1 です。基準の実行(base1)は4つの文書を書き上げましたが、API リファレンスの途中で、サンドボックス環境の URL を https://api.datasync.io/v2/test と自分で作ってしまいました。元資料の開発者メモには https://sandbox.api.datasync.io/v2 と書いてあります。base1 は何度か直そうとしたものの、最終版でも「ホストは開発者ポータルで案内」とぼかしたままでした。 INV 役(やり方の担当)は、この場面を軌跡の行番号つきで指摘し、次のガードレールを書きました。合成役はこれを採用し、出力の検証用スキル(verify-outputs)と、その中で使う検証スクリプト(validate_outputs.py、159行、標準ライブラリだけで動く)もあわせて取り込みました。 !ターミナルでtask1の最終ハーネスのguardrails.mdを表示した画面。複数の文書に共通する値は、元資料から1回だけ決めて全ファイルにそのまま写し、書き終えたら全ファイルを開き直して照合する、という英語のルール。下にtask2の最終ハーネスのガードレールがライブラリのtask1のものと末尾の改行を除いて一致することを示すTrue 手元の実測:task1 で採用されたガードレール(英語のまま)。「共通する値は元資料から1回だけ決め、全ファイルにそのまま写し、書き終えたら全ファイルを開き直して照合する」という内容です。最後の行は、task2 の最終ハーネスがこれをそのまま使ったことを示しています。 4文書のうち正しいサンドボックス URL(sandbox.api.datasync.io/v2)が書かれていた数を、手元で数え直しました。 | 実行 | 正しい URL がある文書 | 作った URL が残った文書 | |---|---|---| | base1(元のハーネス) | 0 / 4 | 0 / 4(最終版ではぼかした) | | inv1(INV 案) | 4 / 4 | 0 / 4 | | dom1(DOM 案) | 0 / 4 | 0 / 4(URL ごと省いた) | | agg1(合成・採用) | 4 / 4 | 0 / 4 | INV 案の決まりを入れた2回の実行だけが、4文書すべてで正しい値をそろえていました。 DOM 役(分野の知識の担当)は、「内部向けと書かれた資料は顧客向けの文書に載せない」「リリースノートは決まった節立てで、該当なしの節も残す」といった技術文書の決まりごとをメモリやガードレールに書きました。ただし、これらは最終的なハーネスには採用されませんでした。振り返り役の記録では、DOM 案は4つの部品を同時に変えていて、どれが効いたのか切り分けられない(confounded)とされています。 結果3:経験は次の仕事に持ち越された task1 の振り返り役は、ライブラリにこう書きました(要約)。 - きっかけ:同じ事実を何度も書く「連動した複数の文書」を作る仕事で、モデルが文書ごとに値を作り直したり、ぼかしたりしがちなとき - 教訓:ガードレールに、共通の値を1回だけ決めて写し、最後に開き直して照合する短い決まりを置く(10行程度)。検証スキルは補助で、決まりの代わりにはしない - 確度:中(効いた対比は1回の実行だけなので、判定は confounded) 続く task2(ペットストア API の移行ガイド・改訂版リファレンス・影響分析 CSV)では、3つの編集役の作業フォルダすべてに、このライブラリが入っていました。合成役(agg1)は判断メモに「ライブラリの task1 のガードレールを、そのまま同じ文面で採用」と書き、実際に task2 の最終ハーネスのガードレール・システムプロンプト・検証スクリプトは、task1 のものと中身が同じでした。検証スキルの説明だけが、task2 で見つかった弱点(CSV を手で書いて数値でない値が混ざった、使ってはいけない旧表記「API Key」が残った)に合わせて書き足されていました。 前の仕事で得た直し方が、次の仕事で「部品ごと選んで再利用」される。論文の言うハーネス改善の再帰が、手元の小さな実験でもそのとおりに動いたことになります。 合成役が「常時読み込む文章は小さく保つ」という指示を守っていたのも印象的でした。task2 の DOM 案は常時読み込む部品を 4,474 バイトまで膨らませていましたが、合成役はそれを採らず、2,249 バイトに抑えています。 結果4:点数 最後に、JobBench の採点基準(ルーブリック)で、元のハーネスの成果物(base1)と、Harness² が採用した成果物(agg1)を採点しました。採点役も同じ Qwen3.8-27B です。JobBench の点数は、採点項目(配点6〜10)ごとに「細かい基準をすべて満たしたら配点を加える」方式で付きます。 !手元の実験のまとめ。task1ではbase1が作ったURLで0/4、INV案のinv1が4/4、DOM案のdom1が0/4、合成したagg1が4/4。振り返りの教訓がライブラリに入り、task2の合成役が同じ文面で採用。採点はtask1が38点から54点へ+24.2、task2が22点から12点へ-16.7 手元の実測です。task1 は大きく上がった一方、task2 は下がりました。 | 課題 | base1(元のまま) | agg1(採用) | 差 | 細かい基準の合格数 | |---|---|---|---|---| | task1(66点満点) | 38点(57.6%) | 54点(81.8%) | +24.2 | 35/40 → 38/40 | | task2(60点満点) | 22点(36.7%) | 12点(20.0%) | −16.7 | 19/32 → 18/32 | | 平均 | 47.1% | 50.9% | +3.8 | — | task1 では、base1 が落とした3項目(dataset.deleted が存在しないイベントであることの説明、レート制限の区分、本番とサンドボックスの URL)を agg1 がすべて取りました。3つめはまさに、INV 役が見つけた URL の問題です。逆に「エンドポイントの版と変更履歴の対応」の1項目だけは agg1 が落としています。 task2 の −16.7 は、配点10の1項目「GET /user/login のレスポンスヘッダーを、型・説明・値の例つきで書いているか」の合否が入れ替わっただけで生じていました。base1 は採点役に「4つのヘッダーすべてに型・説明・例がある」と判定され、agg1 は「型と説明はあるが例の値がない」として、この項目を落としています。細かい基準の数で見ると 19 対 18 でほぼ同じです。逆に agg1 は「よくある移行のつまずき」の項目で基準を1つから3つに増やしていますが、全部は満たせず加点には届きませんでした。 なお、base1 も agg1 も、移行ガイドに curl と Python の変更前後のコード例を書いておらず、配点10の項目を落としています(agg1 にあるコードブロックは URL を示す2つだけでした)。ハーネスを直す側(同じ Qwen)も、この抜けには気づきませんでした。採点役の説明には「コードブロックが1つもない」と事実と少し違う記述もあり、ローカルモデルの採点をうのみにしない方がよいことも分かります。 この結果から言えるのは次の範囲です。 - 言えること:正解を見ずに、実行の対比から「作った値を書かない」「最後に開き直して照合する」という具体的な直しが生まれ、その直しが効く項目では点が上がった(task1)。その経験は、次の課題に部品ごと受け渡された(task2) - 言えないこと:Harness² が手元のモデルで平均的に点を上げるかどうか。各1回の実行で、採点の合否は配点の大きい1項目で大きく振れます。論文も各設定を3回ずつ回して平均しています - 論文との違い:編集役が Claude Opus 4.8 ではなく実行役と同じ 27B のローカルモデルで、スキルの読み込みは後述の理由で失敗していました 気づいた注意点 手元の実験では、結果の読み方に関わる問題が3つありました。 1. スキルの読み込みが全部失敗していた(Mac の OpenCode) 実行役がスキルを呼ぶたびに、ripgrep archive did not contain executable というエラーが出ていました。今回の版の OpenCode は、PATH に rg(ripgrep)が無いと GitHub から取得して一時フォルダに展開しますが、その一時フォルダを片付けてから実行ファイルの有無を確かめているため、必ず失敗します。Linux では rg が入っていることが多く、表に出にくい問題だと思われます。 今回の実行では、エージェントはエラーのあと SKILL.md をファイルとして直接読んで手順に従ったり、同じ確認を自分でスクリプトに書いて実行したりしていました。スキルは「呼べなかったが、中身はおおむね使われた」状態で、論文の環境とは条件が違います。次のどちらかで直ります。 # 方法1:Homebrew で ripgrep を入れる(PATH 上の rg が使われる) brew install ripgrep # 方法2:OpenCode が取得済みのアーカイブから rg を取り出し、OpenCode が探す場所に置く cd ~/.cache/opencode/bin tar -xzf ripgrep-14.1.1-aarch64-apple-darwin.tar.gz ripgrep-14.1.1-aarch64-apple-darwin/rg mv ripgrep-14.1.1-aarch64-apple-darwin/rg ./rg && rmdir ripgrep-14.1.1-aarch64-apple-darwin 方法2で rg を置いたあと、簡単なスキル(SKILL.md に合言葉を書いたもの)を OpenCode に読み込ませると、skill ツールが成功し、合言葉を正しく返しました。これから試す方は、実験の前にこの手当てをしておくことをおすすめします。 2. 採点の時間切れは0点として記録される JobBench の採点器は、採点項目ごとの問い合わせを8本同時に送り、240 秒で返らないと3回まで再試行し、それでもだめならその項目を不合格(0点)として記録します。LM Studio は同時に2本しか処理しないので、そのままだと待たされた項目が「本当は合格かもしれないのに0点」になります。最初は2件を同時に採点していたため計16本が LM Studio に並び、1項目しか終わらないまま25分たったのを見て止め、同時数を2、待ち時間を1時間に変えてやり直しました(run_local.py の追加分)。 # run_local.py に追加:JobBench の採点器に渡す引数を、ローカル向けに書き換える JUDGE_WORKERS = os.environ.get("LOCAL_JUDGE_WORKERS", "2") JUDGE_TIMEOUT = os.environ.get("LOCAL_JUDGE_TIMEOUT", "3600") _run = jobbench.subprocess.run def _judge_run(cmd, *a, **kw): if isinstance(cmd, list) and any(str(c).endswith("judge.py") for c in cmd): cmd = list(cmd) for flag, value in (("--max-workers", JUDGE_WORKERS), ("--timeout-per-rubric", JUDGE_TIMEOUT)): if flag in cmd: cmd[cmd.index(flag) + 1] = value kw["timeout"] = max(int(kw.get("timeout") or 0), 6 * int(JUDGE_TIMEOUT)) return _run(cmd, *a, **kw) jobbench.subprocess.run = _judge_run # 下の分岐の judge のところも、採点を1件ずつにするよう書き換える # elif cmd == "judge": # from harness2.cli import judge as _judge # _judge._PARALLEL = 1 # from harness2.cli.judge import main 途中まで書かれた採点ファイルは、次回そのまま「採点済み」として読まれるので、やり直す前に eval_result/ を消しておく必要があります。 3. ネットの禁止は bash で抜けられる OpenCode の webfetch と websearch は禁止しましたが、エージェントは bash の curl で外部にアクセスしていました。課題が名前を挙げている参考資料(GitHub の gist にある API 文書の見本、Microsoft のスタイルガイドなど)を取りに行ったもので、すべての軌跡を調べた範囲では、Hugging Face 上の採点基準(RUBRICS.json)や JobBench のリポジトリへのアクセスはありませんでした。それでも、ツールの設定だけでは通信を止められないことは確かです。公式ガイドのとおり、評価に使うならファイアウォールや DNS で通信先を塞ぐ必要があります。 トラブルシューティング 手元で実際に引っかかった点と、ソースコードを読んで分かった注意点を、確かめる順にまとめます。とくに④と⑦は、エラーで止まらずに結果だけがゆがむので気をつけてください。 !動かないときの確認順。準備、設定、実行役のtimeout、スキルのripgrep、編集役の思考、採点のトークン、採点の時間切れの7項目と、それぞれの対処 左がログに出る文言、右が対処です。④⑦はエラーで止まらず、結果だけが変わります。 | 症状(ログの文言) | 原因 | 対処 | |---|---|---| | Refusing to overwrite nonempty dependencies/… | 準備が途中で止まった残り | そのフォルダを別名に移して harness2-setup をやり直す | | HARNESS2_SOLVER_MODEL is required … | モデルの指定がない(.env.example は同梱されていない) | lmstudio/qwen/qwen3.8-27b のように「プロバイダー/モデル」で環境変数に書く | | No such file or directory: 'timeout' | macOS に GNU の timeout が無い | 代わりのスクリプトを PATH の先頭に置く(または coreutils) | | TIMEOUT や runner wall-timeout | ローカルモデルが遅く、実行役の上限(OpenCode は 2400 秒)を超えた | 上限を延ばす(今回は 5400 秒) | | ripgrep archive did not contain executable | OpenCode が ripgrep の自動取得に失敗する | brew install ripgrep、または ~/.cache/opencode/bin/rg に置く | | 編集役が非常に遅く、LM Studio に思考が流れる | 編集役は --variant high 付きで呼ばれる | effort を空にして --variant を付けない | | no Vertex access token … judge not run | 採点役が gcloud のトークンを取りに行く | HARNESS2_JUDGE_BASE_URL と HARNESS2_JUDGE_API_KEY を設定する | | 採点項目が時間切れで不合格になる | 8本同時・240 秒の待ち時間で、時間切れは0点として記録 | 同時数を減らし、待ち時間を延ばす。途中の eval_result/ は消してからやり直す | 自分の仕事に載せるには:アダプターを書く Harness² は、ベンチマークごとの違いを アダプター(adapter) という1つのクラスに閉じ込めています。harness2/core/spec.py の BenchmarkAdapter には「これを実装すれば方式全体が動く。ここに方式のロジックは一切ない」と書かれています。自分の業務課題で試したいときに書くのは、このアダプターです。 主なメソッドを表にまとめます。 | メソッド | 返すもの | 書くときの注意 | |---|---|---| | tasks() | 課題 ID の一覧 | 順番を固定する(ライブラリの状態を再現できるように) | | domain_of(task_id) | 分野の名前 | ライブラリはこの単位で分かれる | | task_prompt(task_id) | エージェントに渡す依頼文 | 採点基準や期待する出力は抜いておく | | task_files(task_id) | 元資料のフォルダ | 編集役も context/source/ として読む | | execute(task_id, harness_dir, run_id) | 1回の実行のフォルダ | harness_dir=None は元のハーネス。同じ run_id なら結果を使い回す | | delivered(run_dir) | 成果物ができたか | 何も出なかった実行を見分ける | | trajectory(run_dir) | 読める形の実行記録 | [ACTION]/[OBSERVATION]/[ERROR] の書式にそろえる | | deliverable_text(run_dir) | 成果物の文字列 | エージェントが書いたものだけから取り出す | | base_harness() | 初期ハーネスのフォルダ | 8つの部品のファイルを置く | | validate_tree(tree) | なし(不備なら例外) | 例外のメッセージがそのまま編集役への差し戻し理由になる | | score(run_dir, task_id) | 採点結果 | 選択が終わった後にしか呼ばれない。失敗時は None(0点と区別する) | 付属デモの harness2/demo.py(約160行)が、モデルを使わない最小のアダプターとしてそのまま手本になります。デモでは execute() が Python スクリプトを実行するだけですが、ここを OpenCode や Codex の起動に置き換えれば、自分の課題で同じ流れを回せます。 もう1つ面白い作りがあります。課題の説明文(BenchPrompts)に「benchmark」「evaluation」「grader」「rubric」「score」といった語が入っていると、エラーで止まるようになっているのです。エラーには「測り方ではなく仕事そのものを書け。実務家に必要なことを言え」とあります。アダプターを書く人がうっかり採点の話を指示文に混ぜないための安全装置です。 まとめ Harness² は、AI エージェントのハーネス(指示文・スキル・スクリプトなどの道具立て)を、正解のない仕事の中で AI 自身に直させる枠組みです。要点を振り返ります。 - 正解の代わりに「行動の違い」を証拠にする:基準の実行、2つの直し案(やり方の INV と分野の知識の DOM)の実行を見比べ、証拠のある変更だけを合成します - 直し方の経験を次へ持ち越す:ハーネスそのものは毎回初期状態から始め、採用したハーネス一式と経験メモをライブラリに残して、次の仕事の編集役に読ませます - 論文では一貫して改善:1回の改善で3ベンチマーク・4通りの組み合わせすべてが上がり、k を増やすとさらに伸びました。ただし計算量は増えます - Mac でも動く:付属デモはそのまま動きました。JobBench は、timeout の代わりのスクリプトなど3か所を補えば、LM Studio のローカルモデルだけで最後まで回せました(スキルを正しく読ませるには ripgrep の手当ても必要) - 手元でも経験の持ち越しを確認:task1 で見つかった「ファイル間で値をそろえる」決まりが、ライブラリ経由で task2 のハーネスに取り込まれました - 点数は上下した:task1 は 57.6% → 81.8%、task2 は 36.7% → 20.0%。各1回・採点役も同じ 27B モデルなので、点数の良し悪しは決められません 一方で、採点基準のネット経由の漏れ、実行のたびのゆらぎ、編集役と実行役が同じ思い込みを持つ危うさなど、使う側が気をつけるべき点もはっきりしています。 次の一歩としては、まず harness2-demo で流れをつかみ、付属デモのアダプター(harness2/demo.py)をまねて、自分の小さな業務課題を1つ載せてみるのがおすすめです。採点の仕組みが自分で用意できる課題(決まった形式のレポート、表の集計など)から始めると、改善の効き目を確かめやすくなります。 参考リソース - GitHub:google-research/harness2(Apache 2.0) - 論文:Ruiyao Xu ほか「Harness²: Recursive Agent Harnessing for an Open World」、arXiv 2610.14224(2026年10月9日)arxiv.org - プロジェクトページ:harness2.github.io - リポジトリ内のガイド:docs/SETUP.md(準備)、docs/EXPERIMENTS.md(実験の進め方とネットの制限)、docs/BENCHMARKS.md(各ベンチマーク) - JobBench:Job-Bench/job-bench-eval - OpenCode:anomalyco/opencode(Harness² がピン留めしている版の取得元)