Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
25eaab7
feat(skills): distill run insights into a cross-run skill library
ignorejjj Jun 29, 2026
26e91ad
feat(skills): layer distilled experience by altitude (meta vs domain)
ignorejjj Jun 29, 2026
828ec6e
fix(skills): tag domain from project dir instead of 'general'
ignorejjj Jun 29, 2026
d9fd8f0
feat(skills): domain-scope library recall to prevent negative transfer
ignorejjj Jun 29, 2026
016668c
feat(experience): capture lessons live during the run, fold in at fin…
ignorejjj Jun 29, 2026
4dca950
feat(skills): consolidate experience across runs with occurrence counts
ignorejjj Jun 29, 2026
d5d219e
refactor(experience): per-session EXPERIENCE.md + topic recall, not a…
ignorejjj Jun 29, 2026
a47d36a
feat(experience): surface prior experience at intake + compose for topic
ignorejjj Jun 29, 2026
818b8f9
feat(experience): optional LLM abstraction of lessons into transferab…
ignorejjj Jun 29, 2026
2a37d19
feat(experience): inject composed prior experience into the launched …
ignorejjj Jun 29, 2026
83ff42d
feat(experience): structured decision-ready experience + LLM selectio…
ignorejjj Jun 29, 2026
78aec99
feat(experience): concrete situational findings (A: RecordFinding too…
ignorejjj Jun 29, 2026
3cb28f6
fix(experience): stop the mining-prompt placeholder leaking as the su…
ignorejjj Jun 29, 2026
b6393e3
feat(experience): merge findings across recalled sessions with recurr…
ignorejjj Jun 29, 2026
0762f62
docs(readme): News — Arbor learns from its own runs (experience captu…
ignorejjj Jun 29, 2026
cdcdede
docs(readme): tighten the self-evolution News line; sync zh-CN
ignorejjj Jun 30, 2026
de924ae
Merge remote-tracking branch 'origin/main' into feat/skill-distill
ignorejjj Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ https://github.com/user-attachments/assets/49c1a306-d2e9-49d6-9c83-65e38a62df30

## 📣 News

- **2026-06-30** — **Arbor learns from its own runs.** Each run leaves concrete, reusable findings — a dataset quirk that helped, a trap to avoid; the next similar task recalls them at intake, so the agent starts from experience instead of scratch. 🧠
- **2026-06-22** — **Built-in literature search & idea novelty checks.** Arbor can now ground its research in prior work via the public [alphaXiv](https://www.alphaxiv.org) API — zero config, no search endpoint or key. Novelty-check any idea before you build it with `arbor idea-check "<your idea>"`, or let the Coordinator vet every new branch automatically. See [Literature Search & Novelty Checks](#-literature-search--novelty-checks). 🔎
- **2026-06-18** — Arbor was featured by [VentureBeat](https://venturebeat.com/), one of the leading tech media outlets in the US: ["New AI optimization framework beats Claude Code and Codex by 2.5x on the same compute budget"](https://venturebeat.com/orchestration/new-ai-optimization-framework-beats-claude-code-and-codex-by-2-5x-on-the-same-compute-budget). 📰
- **2026-06-12** — Arbor's native CLI runtime and Agent Skill Suite (Codex / Claude Code) are released. 🚀
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

## 📣 最新动态

- **2026-06-30** — **Arbor 会从自己的运行中学习。** 每次运行都会留下具体、可复用的发现——数据集上能提分的特性、需要避开的坑;下次类似任务在 intake 时把它们召回,于是 agent 从经验出发,而不是从零开始。🧠
- **2026-06-22** — **内置文献检索与想法新颖性审查。** Arbor 现在可以通过 [alphaXiv](https://www.alphaxiv.org) 公共 API 把研究建立在已有工作之上——零配置,无需搜索端点或密钥。动手前用 `arbor idea-check "<你的想法>"` 审查任意想法的新颖性,或让 Coordinator 自动为每个新分支把关。详见[文献检索与新颖性审查](#-文献检索与新颖性审查)。🔎
- **2026-06-18** — Arbor 被美国知名科技媒体 [VentureBeat](https://venturebeat.com/) 报道:[《New AI optimization framework beats Claude Code and Codex by 2.5x on the same compute budget》](https://venturebeat.com/orchestration/new-ai-optimization-framework-beats-claude-code-and-codex-by-2-5x-on-the-same-compute-budget)。📰
- **2026-06-12** — Arbor 原生 CLI 运行时与智能体技能套件(Codex / Claude Code)正式发布。🚀
Expand Down
35 changes: 34 additions & 1 deletion src/cli/intake/launch_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,17 @@ class LaunchExperimentTool(Tool):
"items": {"type": "string"},
"description": "Optional bullet notes (constraints, edge cases) to attach to the plan.",
},
"apply_experience": {
"type": "boolean",
"description": "Set true when the user agreed to reuse prior experience; "
"matched lessons are composed into the instruction.",
},
"experience_sessions": {
"type": "array",
"items": {"type": "string"},
"description": "Session names (from 'Prior experience available') you judged "
"relevant to this goal. You select; keyword match is only a fallback.",
},
},
"required": ["cwd", "instruction"],
}
Expand Down Expand Up @@ -169,7 +180,9 @@ async def execute(self, **kwargs: Any) -> str:

plan = LaunchPlan(
cwd=str(resolved.resolve()),
instruction=instruction,
instruction=_with_experience(str(resolved.resolve()), instruction,
kwargs.get("apply_experience"),
kwargs.get("experience_sessions")),
rationale=(kwargs.get("rationale") or "").strip(),
suggested_max_cycles=_safe_int(kwargs.get("suggested_max_cycles")),
suggested_max_turns=_safe_int(kwargs.get("suggested_max_turns")),
Expand All @@ -192,3 +205,23 @@ def _safe_int(v: Any) -> int | None:
return int(v) if v is not None else None
except (TypeError, ValueError):
return None


def _with_experience(cwd: str, instruction: str, apply: Any, sessions: Any = None) -> str:
"""Prepend composed prior experience to the instruction when the user opted in.

Prefers the sessions the intake agent (an LLM) selected as relevant; falls back
to keyword topic matching only if it named none.
"""
if not apply:
return instruction
try:
from ...recall import compose_for_topic, compose_from_sessions
block = ""
if isinstance(sessions, list) and sessions:
block = compose_from_sessions(cwd, [str(s) for s in sessions])
if not block:
block = compose_for_topic(cwd, instruction)
except Exception: # pylint: disable=broad-exception-caught
block = ""
return f"{block}\n\n---\n{instruction}" if block else instruction
23 changes: 23 additions & 0 deletions src/cli/intake/system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@
from __future__ import annotations


def _prior_experience_block(starting_cwd: str) -> str:
"""If past runs here left experience, tell intake to offer it and ask the user."""
try:
from ...recall import list_experiences
exps = list_experiences(starting_cwd)
except Exception: # pylint: disable=broad-exception-caught
exps = []
if not exps:
return ""
lines = [f" - {name}: {desc}" for name, desc in exps]
return (
"## Prior experience available\n"
"This project has experience from past runs:\n"
+ "\n".join(lines) + "\n"
"If the user's goal is similar, briefly offer to reuse the relevant one "
"and ask whether to apply it (default yes). Don't push it if unrelated.\n"
"When launching with the user's agreement, set apply_experience=true and "
"list the relevant session names in experience_sessions (you judge relevance).\n"
"\n"
)


def build_system_prompt(*, starting_cwd: str) -> str:
"""Build the planning-agent system prompt.

Expand Down Expand Up @@ -101,6 +123,7 @@ def build_system_prompt(*, starting_cwd: str) -> str:
"## Target directory\n"
f"The user launched you from: {starting_cwd}\n"
"\n"
+ _prior_experience_block(starting_cwd) +
"Treat that path as the default target. In the common case the user "
"is already inside their project — just go with it and start talking "
"about the goal. Quietly check it looks like a project (one Glob is "
Expand Down
4 changes: 4 additions & 0 deletions src/coordinator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,10 @@ class CoordinatorConfig(ProxyModel):
export_trajectory: bool = True
# Capture per-call token-level traces (tokens.jsonl) for SFT/RL. Heavy; off.
token_trace: bool = False
# Distill run insights into a reusable skill in ~/.arbor/skills (line 2). Off.
distill_skills: bool = False
# Use the LLM to abstract distilled lessons into transferable principles. Off.
distill_abstract: bool = False

# ── Plugin (runtime object; not serialized) ──────────────────────
plugin: Any = PydField(default=None, exclude=True, repr=False)
Expand Down
10 changes: 10 additions & 0 deletions src/coordinator/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,16 @@ def _write_run_stats(
except Exception as e: # pylint: disable=broad-exception-caught
_print_status(f"Warning: failed to write trajectory: {e}")

# Self-evolution line 2: distill insights into a reusable cross-run skill.
if getattr(self.config, "distill_skills", False):
try:
from ..distill import distill_to_session
prov = self.provider if getattr(self.config, "distill_abstract", False) else None
p = distill_to_session(self.config.workspace_dir, provider=prov)
_print_status(f"Distilled experience: {p}" if p else "Distill: nothing to learn")
except Exception as e: # pylint: disable=broad-exception-caught
_print_status(f"Warning: failed to distill skill: {e}")

# ------------------------------------------------------------------
# Final report
# ------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions src/coordinator/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .search_ctx import SearchIdeaContextTool, SearchIdeaContextParallelTool, SearchStatusTool
from .research_ctx import ResearchSearchTool
from .ask_user import AskUserTool
from .record_finding import RecordFindingTool
from ..convergence import ConvergenceDetector, ConvergenceConfig

if TYPE_CHECKING:
Expand Down Expand Up @@ -85,6 +86,7 @@ def get_coordinator_tools(
FileReadTool(cwd=cwd, workspace_dir=wdir),
GrepTool(cwd=cwd, workspace_dir=wdir),
GlobTool(cwd=cwd, workspace_dir=wdir),
RecordFindingTool(cwd=cwd, workspace_dir=wdir),
]
if skill_registry is not None:
# Skills (read-only reference docs loaded on demand)
Expand Down
55 changes: 55 additions & 0 deletions src/coordinator/tools/record_finding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""RecordFinding — let the agent jot a concrete, situational discovery mid-run.

This is the live half (A) of experience capture. When the coordinator or executor
notices something specific and worth remembering for next time — a dataset quirk
that helps the metric, a trap an executor fell into, a gotcha in the harness — it
records it here. Kept specific on purpose; finalize also mines the trajectory (B)
for findings that were never explicitly logged.
"""

from __future__ import annotations

from typing import Any

from ...core.tools.base import Tool


class RecordFindingTool(Tool):
"""Record a concrete discovery or pitfall for future runs to reuse."""

name = "RecordFinding"
is_read_only = True # only appends to a notes file; safe to parallelize
description = (
"Record a CONCRETE, situational discovery worth remembering next time you "
"work on this dataset / task / harness. Two typical kinds:\n"
"- 'leverage': a specific property you found that helps the metric "
"(e.g. 'this dataset's labels are noisy above index 9000 — drop them').\n"
"- 'pitfall': a specific trap to avoid (e.g. 'the executor kept editing "
"eval.py — remind it the harness is protected').\n"
"Keep it specific — the value is the detail, not a general principle. Do "
"NOT log routine progress or generic advice."
)
input_schema: dict[str, Any] = {
"type": "object",
"properties": {
"kind": {"type": "string", "description": "'leverage' or 'pitfall' (free-form ok)."},
"about": {"type": "string", "description": "What it concerns: the dataset, an executor, the harness, etc."},
"note": {"type": "string", "description": "The concrete finding, in one or two sentences."},
},
"required": ["note"],
}

def __init__(self, *, cwd: str, workspace_dir: str | None = None):
super().__init__(cwd=cwd, workspace_dir=workspace_dir)

async def execute(self, **kwargs: Any) -> str:
note = (kwargs.get("note") or "").strip()
if not note:
return "Error: 'note' is required."
try:
from ...experience import record_finding
record_finding(self.workspace_dir or self.cwd,
kind=kwargs.get("kind", ""), about=kwargs.get("about", ""), note=note)
except Exception as exc: # pylint: disable=broad-exception-caught
return f"Could not record finding: {exc}"
return f"Recorded {kwargs.get('kind') or 'finding'}: {note[:80]}"
16 changes: 16 additions & 0 deletions src/coordinator/tools/tree_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,22 @@ async def execute(self, **kwargs: Any) -> str:

self._tree.update_node(node_id, **updates)

# Live experience capture (self-evolution): log lessons as the research
# advances, not only at finalize. Best-effort — never break a tool call.
if updates.get("insight") or updates.get("status"):
try:
from ...experience import append_experience
# Use the node's current insight (it may have been set by a prior
# update), not just this call's fields, so notes aren't empty.
node = self._tree.get_node(node_id)
enriched = dict(updates)
if not enriched.get("insight") and node is not None and getattr(node, "insight", ""):
enriched["insight"] = node.insight
append_experience(getattr(self._tree, "json_path", None),
node_id=node_id, updates=enriched)
except Exception: # pylint: disable=broad-exception-caught
pass

parts = [f"Updated {node_id}:"]
for k, v in updates.items():
parts.append(f" {k} = {v}")
Expand Down
Loading
Loading