Prior Work Deep-Dive · 소스 코드 근거

symbolica bestiary (agentica) — 프롬프트가 지식 경계를 긋는 멀티에이전트

이 페이지의 모든 인용은 engine/agents/templates/agentica/ (prompts.py, scope/roles.py, skills/, sleep/, wake/) 원문에서 그대로 가져왔다. 우리 엔진은 이 코드베이스를 포크한 것이므로, 여기 적힌 것은 “선행 연구”이자 동시에 우리 계보다.

01

시스템 한 줄 정의

Agentica SDK 위의 ARC-AGI-3 하네스(Arcgentica)로, 게임을 직접 하지 않는 오케스트레이터 LLM이 explorer · theorist · tester · solver 서브에이전트를 스폰해 굴리고, 공유 memories와 사후확률 달린 skills 라이브러리가 에이전트 수명을 넘어 지식을 이어 나르는 시스템이다. wake(게임 플레이)와 sleep(스킬 라이브러리 최적화)이 동일한 Phase 라이프사이클로 대칭 구동되고, sleep 안에서는 miner(생성)와 judge(평가)가 분리된 최적화 루프가 돈다.

지식이 게임 구현에서 새어 들어오는 것을 막는 장치가 코드가 아니라 프롬프트 텍스트다: 모든 서브에이전트에게 반드시 전달되는 GAME_REFERENCE 상수가 “에이전트가 알아도 되는 것의 전부”를 한 문서로 고정한다.

02

GAME_REFERENCE — 지식 누출 경계

prompts.py — GAME_REFERENCE (모듈 상수, f-string; ~line 437부터 정의)

인터페이스 계약(64×64 정수 그리드, 액션 6종, RESET 의미론, Frame 헬퍼 API)까지만 알려주고, 게임별 메카닉은 단 한 글자도 넣지 않는다. 핵심 발췌(원문):

원문 펼치기 — GAME_REFERENCE: 관계 기반 사고 강제 (prompts.py)
This is a visual game designed for humans. You see it as a 64x64
coordinate grid of integers 0-15 ({COLOR_LEGEND}), due to the nature and limitations
of your interface. You use coordinates to identify positions and click, but game
mechanics and win conditions are about relationships between elements, not positions
on the grid. Think "A must reach B" not "A must reach row 38." If your
hypothesis includes a specific coordinate as part of the goal, it is wrong --
restate it in terms of what must relate to what.
Render the grid and read it as a picture.
원문 펼치기 — GAME_REFERENCE: Forming good hypotheses (prompts.py)
Forming good hypotheses: When something interesting happens, don't just note the
  event -- note what else was true at that moment. What were other elements doing
  relative to each other? The relationship between elements is often the actual rule.
  Never trust a hypothesis based on a single observation. Reproduce the effect from
  a different starting state to separate the actual rule from coincidences of that
  particular configuration.

경계가 “문서 하나”이기 때문에 누출 실험도 이 파일 안에서 게이트로 관리된다. 예컨대 A3_DECODE_STATE=1일 때만 _DECODE_STATE_BLOCK (특정 게임의 코드-튜플 잠금 상태를 풀어 쓴 블록)이 GAME_REFERENCE에 주입되고, 기본값에서는 “the emitted prompt is byte-identical to the current un-gated text”(소스 docstring 원문)가 보장된다. 즉 “무엇이 새는가”가 항상 diff 가능한 한 지점에 모여 있다.

전달 규약도 프롬프트에 명문화되어 있다: 오케스트레이터 premise 원문 — “Always include GAME_REFERENCE in the system prompt”, “Always pass GAME_REFERENCE=GAME_REFERENCE, history=history, and memories=memories to every subagent.” 새 서브에이전트는 아무것도 모른다는 전제(“A new subagent knows NOTHING about the game.”)가 경계의 반대면이다.

03

THEORIST premise — 가설 규율: 메커니즘, 좌표 금지

scope/roles.py — _THEORIST_PREMISE (원문 전문)

원문 펼치기 — _THEORIST_PREMISE 전문 (scope/roles.py)
You are the THEORIST. The orchestrator calls you once before each acting turn
to propose hypotheses for HOW TO CLEAR THIS LEVEL -- your reasoning is folded
into the trace so the skill-miner and the judge can see whether a skill
ADVANCES a hypothesis you named (vs repeating an exhausted one).

Read the trace + memories. Propose 2-3 concrete HYPOTHESES for how to CLEAR
THIS LEVEL -- mechanic-based, NEVER absolute coordinates. Each hypothesis is one
testable sentence: name the MECHANIC and the OBSERVATION that would confirm or
refute it. Mark any hypothesis the memories show was already tried with no goal
change as EXHAUSTED, and prefer a structurally different one.

YOU MUST NOT call spawn_agent, submit_action, eval/exec/subprocess.

세 가지 규율이 문장 단위로 박혀 있다. ① 메커니즘 기반, 절대좌표 금지 — 가설은 “무엇이 무엇과 관계 맺어야 하는가”로만 서술한다(GAME_REFERENCE의 “A must reach B, not row 38”과 같은 축). ② 반증 가능성 — 가설 하나 = 검증 가능한 한 문장이며, 확인/반증할 관측을 함께 명명해야 한다. ③ 소진(EXHAUSTED) 표시 — 이미 시도되어 목표 변화가 없었던 가설은 소진으로 표시하고 구조적으로 다른 가설을 우선한다. theorist의 추론은 trace에 접혀 들어가 miner와 judge의 채점 기준(hypothesis progress)이 된다.

스폰 조건(소스 주석 원문): “Spawned (and CALLED) once per acting turn ONLY when A3_THEORIST_PROGRESS is on (agent.py wake); OFF => never spawned. It is NOT a sleep-internal role.” 오케스트레이터 premise의 Hypothesize 단계에도 같은 규율이 있다: “Reject any hypothesis stated in absolute coordinates -- mechanics are always relational.”

SKILL-AGENT (miner) premise — 증거 프로토콜

scope/roles.py — _SKILL_AGENT_PREMISE 발췌

원문 펼치기 — _SKILL_AGENT_PREMISE: EVIDENCE PROTOCOL (scope/roles.py)
EVIDENCE PROTOCOL (mandatory):
  ...
  2. Ground each candidate in `evidence`: >=10-word verbatim quotes from
     memory.details or trace_digest lines.
  3. Write task-agnostic, coordinate-free skills: never reference a task
     name, a level number, or a grid coordinate -- the judge scores these as
     grounding failures.

miner의 산출물은 list[CandidateSpec]이고, 각 후보는 one-liner가 아니라 스킬 문서다: recipewhen → do → expect → verify → avoid 다섯 절을 채워야 하며, verify[EXPLOIT]/[EXPLORE] 방향 태그로 시작해 “무엇이 확증 관측인가”를 명시해야 한다.

04

스킬 DB 레코드 — 모든 지식에 사후확률

skills/skill.py — @dataclass(slots=True, frozen=True) class Skill (필드 원문)

원문 펼치기 — Skill dataclass 필드 (skills/skill.py)
@dataclass(slots=True, frozen=True)
class Skill:
    skill_id: str                                  # stable UUID4
    summary: str                                   # one-line description
    recipe: str                                    # when->do->expect->verify->avoid
    evidence: list[str]                            # memory-id / trace 인용 참조
    posterior: tuple[int, int]                     # (confirm_n, falsify_n) Beta-style counts
    applicability_conditions: list[str]            # acting-time 적용 전제조건
    parent_hypothesis_ids: list[str]
    category: str                                  # mechanic|strategy|metacognitive|meta_skill|heuristic|invariant
    quarantined: bool = False
    quarantine_reason: str | None = None
    timestamp: datetime
    origin_task / origin_run_id                    # 크로스-게임 계보 추적
    retrieval_labels: tuple[str, ...]              # 검색자-어휘 라벨 (T3)
    posterior_by_game: dict                        # 게임별 confirm/falsify 분리 (T7)
    provenance: Provenance                         # scope 사다리: level|game|family|universal

JSONL 한 줄로 왕복 직렬화(to_record/from_record)되고, 에피소드 경계 생존은 순수 술어 하나로 판정된다:

원문 펼치기 — is_surviving() 생존 술어 (skills/skill.py)
def is_surviving(self, *, game: str | None = None) -> bool:
    # A skill survives the episode boundary iff:
    #   * not quarantined
    #   * confirm_n > falsify_n   (positive net evidence)
    #   * confirm_n + falsify_n >= 3   (minimum support, one-shot promotion 방지)
    c, f = post
    return (not self.quarantined) and (c > f) and (c + f >= 3)

사후확률 갱신은 Skills.confirm(skill_id_or_index, evidence_ref) / Skills.falsify(skill_id_or_index, reason, evidence_ref)로만 이루어지고, falsify에서 falsify_n > confirm_n이 되는 순간 자동 격리된다 — 삭제가 아니라 격리다(소스 docstring 원문): “it is kept in the stack (so it can still surface as a cautionary anti-pattern) but flagged so callers do not reuse it as-is.”

입력 → 출력 예시 (to_record 한 줄의 형태)

예시 펼치기 — confirm() → JSONL 레코드 형태
skills.confirm("3f2a...", evidence_ref="mem_a1b2c3d4")
# skills.jsonl에 append되는 레코드(요약):
{"skill_id": "3f2a...", "summary": "...", "posterior": [3, 1],
 "posterior_by_game": {"ft09": [2, 0]}, "quarantined": false,
 "scope": "game", "provenance": {...}, "op": "confirm", ...}
05

compute_judge — 채점 블록과 fail-closed 게이트

skills/skill_judge.py — JudgeScore (원문 발췌) + scope/roles.py의 채점 블록

원문 펼치기 — JudgeScore dataclass (skills/skill_judge.py)
@dataclass(frozen=True, slots=True)
class JudgeScore:
    advantage: float = 0.0    # 0..1 -- dIG(edit | library, memories, trace)
    grounding: float = 0.0    # 0..1 -- claims evidence-checked vs trace/memories
    combined: float = 0.0     # advantage x grounding, __post_init__에서 재계산(LLM 보고값 무시)
    weaknesses: str = ""      # single strongest doubt -- miner의 rewrite gradient (텍스트)
    direction: dict = ...     # {mode: "exploit"|"explore", sign: +1|-1, ...} 다음 스텝 방향

    def __post_init__(self):
        object.__setattr__(self, "combined",
            float(self.advantage) * float(self.grounding))

judge는 “하나의 저지, 두 시간축”이다(파일 docstring 원문 요지): sleep 안에서는 편집의 기대 정보이득을, sleep 사이(cross-sleep)에서는 메모리-반사실 필요성 (“그 스킬이 없었다면 지금 축적된 memories로부터 다시 유도해야 했는가”)을 실행 없이 판정한다. premise는 채점 코드를 쓰지 말라고 명시한다: “you MUST NOT write or run scoring code — you ARE the score.”

모드-조건 채점 블록 (기본: novel-state proxy)

scope/roles.py — _NOVEL_STATE_SCORING (원문 발췌)

원문 펼치기 — _NOVEL_STATE_SCORING 채점 블록 (scope/roles.py)
MODE-CONDITIONED SCORING -- advantage is PROGRESS-CONDITIONED on a CONCRETE
proxy: reaching a NOVEL (unseen) state. Stuck play has 0 level-ups, so a novel
state stands in for level progress; ...
  EXPLORE -- advantage means expected NEW information; weigh novelty above
    coverage; ... The probe earns HIGH advantage only when it is an
    untried move TOWARD a NOVEL (unseen) state ...
  EXPLOIT -- advantage means compression delivered; weigh coverage (how many
    observations it absorbs) above novelty; ... compressing a pattern
    that keeps the agent in the SAME state -- churn ... is LOW advantage
    no matter how consistent it is.

A3_THEORIST_PROGRESS가 켜지면 이 블록이 _HYPOTHESIS_PROGRESS_SCORING으로 교체된다 — “novel state 도달” 대신 “trace에 있는 theorist 가설을 전진/검증시키는가”가 advantage의 정의가 된다. 블록은 scoring_block() 한 곳에서만 선택되어 in-sleep premise와 cross-sleep task가 항상 같은 기준으로 채점한다(소스 주석: “No second copy of either block exists anywhere”).

fail-closed 커밋 규칙

“a skill commits ONLY on a confident verdict (combined >= commit_conf). Every other outcome — low score, ‘pending’, or NO verdict (the judge timed out / errored …) — resolves WITHOUT committing. An unresolved spec is force-dropped at the horizon.” skill_judge.py docstring 원문

sleep 오케스트레이터가 라이브러리를 직접 쓰는 것도 막혀 있다: _SLEEP_SKILLS_READ_METHODS = frozenset({"summaries", "get", "query", "stack", "is_empty"}) — add/rewrite/deprioritize/confirm/falsify 등 모든 뮤테이터는 차단되고, 쓰기는 park → judge 게이트를 경유하는 commit_skill 클로저로만 흐른다 (실제 라이브 런에서 LLM이 skills.add를 직접 불러 0.49 < τ 편집이 라이브러리에 들어간 사고가 이 차단의 기원이라고 sleep_phase.py 주석에 남아 있다).

06

멀티에이전트 오케스트레이션 스케치

오케스트레이터 premise 첫 줄(원문): “Coordinate subagents. You are a manager, not a player.” 오케스트레이터에게는 submit_action이 아예 없고, 대신 make_bounded_submit_action(limit)으로 액션 예산이 박힌 카운터를 만들어 서브에이전트에게 쥐여 준다 — 예산은 함수 자체에 붙으므로 서브-서브에이전트를 스폰해도 리셋되지 않는다.

Arcgentica (agent.py) — wake/sleep을 같은 run_phase 드라이버로 구동 WakePhase: 한 step = 오케스트레이터 .call 1회 (게임 클리어 / 예산 소진 / 무진행까지 반복). SleepPhase: 한 step = sleep 오케스트레이터 pass 1회
↓ wake: spawn_agent(system_prompt[, role]) + GAME_REFERENCE/history/memories 의무 전달
orchestration phases (premise 원문의 6단계) 1 Explore(explorer + bounded submit_action) → 2 Hypothesize(theorist, no submit_action) → 3 Test(작은 예산으로 표적 실험) → 4 Iterate(같은 theorist 재호출 vs 새 theorist) → 5 Solve(solver) → 6 Next level(새 explorer로 재평가)
↓ 전 에이전트 공유
memories (add/summaries/query) + skills (posterior 달린 라이브러리) 에이전트 컨텍스트는 죽어도 memories/skills는 게임 전체를 산다. 은퇴 직전 “debrief”로 암묵지까지 회수
↻ sleep: miner(skill_agent)가 CandidateSpec 제안 → compute_judge가 JudgeScore → combined ≥ commit_conf만 커밋 (generator ≠ evaluator)

역할 레지스트리(scope/roles.py)의 라이브 role은 단 셋이다: skill_agent(miner) · compute_judge · theorist. explorer/tester/solver는 role 상수가 아니라 오케스트레이터가 자유 문자열 system_prompt로 스폰하는 프롬프트-정의 에이전트다. 세 role premise 모두 마지막 줄이 동일하다: “YOU MUST NOT call spawn_agent, submit_action, eval/exec/subprocess.”

07

우리가 채택한 것 / 채택하지 않은 것

✓ 채택

  • 누출 경계(GAME_REFERENCE) — “에이전트가 게임에 대해 알아도 되는 것”을 단일 상수 문서로 고정하고, 모든 실험적 주입을 env 게이트 + byte-identical 기본값으로 관리하는 규율. 우리 엔진의 프롬프트 경계는 이 설계를 그대로 잇는다.
  • 가설 규율(hypothesis discipline) — 메커니즘 기반 · 절대좌표 금지 · 가설 = 반증 가능한 한 문장 + 확증 관측 명명 · 소진 가설 표시. THEORIST premise와 GAME_REFERENCE의 “Forming good hypotheses” 절을 우리 룰 마이닝의 서술 규격으로 채택했다.

✗ 비채택

  • 멀티에이전트 매니저 — “manager, not a player” 오케스트레이터가 explorer/theorist/tester/solver를 스폰하고 예산을 배분하는 계층 전체. 우리는 단일 code-native 루프를 유지하고, 검증은 LLM 저지가 아니라 엔진 fork 실행으로 대체했다.