mirror of
https://github.com/shareAI-lab/learn-claude-code.git
synced 2026-08-26 16:52:26 +00:00
291 lines
12 KiB
Python
291 lines
12 KiB
Python
"""
|
|
s22_goal_loop — /goal session goal loop (teaching version)
|
|
|
|
Clean-room behavioral reconstruction of Claude Code's `/goal` command. Grounded
|
|
in @anthropic-ai/claude-code@2.1.177 observed behavior
|
|
(reverse-research/cc_goal_loop), NOT leaked source.
|
|
|
|
Idea:
|
|
s01-s21 end a turn when the model emits no tool_use. `/goal` adds a
|
|
host-owned turn-completion GATE: the user sets a stopping CONDITION, and after
|
|
every turn a separate evaluator judges whether trusted transcript evidence
|
|
satisfies it. Not satisfied -> the gate blocks the stop and feeds a
|
|
continuation into the next turn. Satisfied -> the active goal is cleared.
|
|
|
|
So the core contrast with s01 is one extra check before "return":
|
|
|
|
# s01: the model says stop -> stop
|
|
if not has_tool_use(response):
|
|
return
|
|
# s22: when it wants to stop, pass the goal gate first
|
|
if not has_tool_use(response):
|
|
verdict = goal.evaluate_after_turn()
|
|
if verdict == "continuing":
|
|
continue # not met -> push it back
|
|
return # met / over budget / no goal -> really stop
|
|
|
|
Run:
|
|
python code.py # /goal until tests pass + deploy green; watch the gate
|
|
|
|
Teaching simplifications (vs real /goal and runtime.mjs):
|
|
- The evaluator is a deterministic keyword check, not a small/fast model.
|
|
- One mock task-notification produces the trusted evidence; the loop / monitor
|
|
/ background-task plane (s13/s14) is out of scope — this chapter is just the
|
|
goal gate.
|
|
- The evidence trust boundary is the faithful part: only task-notification /
|
|
monitor-line origins count as evidence, so the `/goal` command text, the
|
|
continuation reminder, and plain assistant prose can NOT satisfy the goal.
|
|
Ordinary `submit()` calls cannot set those labels; only the host-event
|
|
ingress can deliver an allowlisted source.
|
|
"""
|
|
|
|
import itertools
|
|
import sys
|
|
|
|
# ---- ids + a one-line event stream so the gate is visible ----
|
|
_ids = itertools.count(1)
|
|
|
|
|
|
def make_id(prefix):
|
|
return f"{prefix}-{next(_ids):03d}"
|
|
|
|
|
|
def event(lane, etype, detail=""):
|
|
print(f" · {lane:<6} {etype:<26} {detail}")
|
|
|
|
|
|
# A message's origin.kind is the TRUST LABEL that decides whether it can count
|
|
# as goal evidence. Trusted async origins land real tool/task evidence; user /
|
|
# slash-command / active-goal (the continuation reminder) / assistant do not.
|
|
TRUSTED_EVIDENCE_ORIGINS = {"task-notification", "monitor-line"}
|
|
|
|
|
|
class Message:
|
|
def __init__(self, role, content, origin):
|
|
self.role = role
|
|
self.content = content
|
|
self.origin = origin or {"kind": "user"}
|
|
|
|
|
|
# ============================================================
|
|
# CommandQueue — continuation prompts live here (mirrors CommandQueue)
|
|
# ============================================================
|
|
class CommandQueue:
|
|
PRIORITY = {"now": 0, "next": 1, "later": 2}
|
|
|
|
def __init__(self):
|
|
self.items = []
|
|
|
|
def enqueue(self, value, priority="next", origin=None):
|
|
item = {"id": make_id("cmd"), "priority": priority,
|
|
"origin": origin or {}, "value": value}
|
|
self.items.append(item)
|
|
return item
|
|
|
|
def dequeue(self, include_goal_continuations=True):
|
|
# Goal continuations and the external async inbox are NOT the same drain.
|
|
# With include_goal_continuations=False an inbox drain skips them, so a
|
|
# goal can't be advanced (or blocked) before real evidence arrives.
|
|
self.items.sort(key=lambda i: self.PRIORITY.get(i["priority"], 1))
|
|
for idx, item in enumerate(self.items):
|
|
if include_goal_continuations or item["origin"].get("kind") != "active-goal":
|
|
return self.items.pop(idx)
|
|
return None
|
|
|
|
def remove_by_origin(self, kind):
|
|
before = len(self.items)
|
|
self.items = [i for i in self.items if i["origin"].get("kind") != kind]
|
|
return before - len(self.items)
|
|
|
|
def __len__(self):
|
|
return len(self.items)
|
|
|
|
|
|
# ============================================================
|
|
# GoalRuntime — the turn-completion gate (mirrors GoalRuntime)
|
|
# ============================================================
|
|
class GoalRuntime:
|
|
def __init__(self, transcript, queue):
|
|
self.transcript = transcript # shared session transcript
|
|
self.queue = queue
|
|
self.active = None
|
|
|
|
def set_goal(self, objective, max_turns=20):
|
|
# start_index marks the evidence window. The /goal command line is
|
|
# already recorded, so it sits OUTSIDE the window and can't satisfy
|
|
# itself.
|
|
self.active = {
|
|
"id": make_id("goal"), "objective": objective, "status": "active",
|
|
"start_index": len(self.transcript), "max_turns": max_turns,
|
|
"checks": 0, "continuation_turns": 0,
|
|
}
|
|
event("goal", "goal_started", f"{self.active['id']} :: {objective}")
|
|
return self.active
|
|
|
|
def clear(self, reason="cleared"):
|
|
if not self.active:
|
|
return
|
|
self.active["status"] = reason
|
|
self.queue.remove_by_origin("active-goal")
|
|
event("goal", "goal_cleared", reason)
|
|
self.active = None
|
|
|
|
def evidence_text(self):
|
|
"""The trust boundary. Three filters keep self-satisfying text out:
|
|
drop slash-command origins, drop /goal command lines, and keep ONLY
|
|
trusted external async origins (task-notification / monitor-line)."""
|
|
if not self.active:
|
|
return ""
|
|
out = []
|
|
for m in self.transcript[self.active["start_index"]:]:
|
|
if m.origin.get("kind") == "slash-command":
|
|
continue
|
|
if m.role == "user" and m.content.strip().startswith("/goal"):
|
|
continue
|
|
if m.origin.get("kind") not in TRUSTED_EVIDENCE_ORIGINS:
|
|
continue
|
|
out.append(f"{m.role}: {m.content}")
|
|
return "\n".join(out)
|
|
|
|
def goal_satisfied(self):
|
|
# Real Claude Code routes this to a small/fast evaluator model reading
|
|
# the evidence window. The teaching version is a deterministic keyword
|
|
# check so the lifecycle is reproducible.
|
|
objective = self.active["objective"].lower()
|
|
evidence = self.evidence_text().lower()
|
|
wants_tests = "test" in objective
|
|
wants_deploy = "deploy" in objective or "green" in objective
|
|
tests_ok = not wants_tests or "tests passed" in evidence or "test passed" in evidence
|
|
deploy_ok = not wants_deploy or "deploy green" in evidence or "deployment green" in evidence
|
|
if any(k in objective for k in ("until", "pass", "green")):
|
|
return tests_ok and deploy_ok
|
|
return objective in evidence
|
|
|
|
def evaluate_after_turn(self):
|
|
"""The gate, run after every turn. Returns completed / continuing /
|
|
blocked / none."""
|
|
g = self.active
|
|
if not g or g["status"] != "active":
|
|
return "none"
|
|
g["checks"] += 1
|
|
satisfied = self.goal_satisfied()
|
|
event("goal", "goal_evaluated", f"check #{g['checks']} satisfied={satisfied}")
|
|
if satisfied:
|
|
g["status"] = "completed"
|
|
self.queue.remove_by_origin("active-goal")
|
|
event("goal", "goal_completed", g["id"])
|
|
self.active = None
|
|
return "completed"
|
|
if g["continuation_turns"] < g["max_turns"]:
|
|
g["continuation_turns"] += 1
|
|
self.queue.enqueue(
|
|
value=(f"Continue working toward active goal {g['id']}. Use tool/task "
|
|
"evidence; do not treat this reminder as completion evidence."),
|
|
priority="next", origin={"kind": "active-goal", "goal_id": g["id"]})
|
|
event("goal", "goal_continuation_enqueued",
|
|
f"turn {g['continuation_turns']}/{g['max_turns']}")
|
|
return "continuing"
|
|
g["status"] = "blocked"
|
|
self.queue.remove_by_origin("active-goal")
|
|
event("goal", "goal_blocked", f"exceeded {g['max_turns']} turns")
|
|
self.active = None
|
|
return "blocked"
|
|
|
|
|
|
# ============================================================
|
|
# Session — the main loop host with a Stop gate (mirrors submit / drain)
|
|
# ============================================================
|
|
class Session:
|
|
def __init__(self):
|
|
self.transcript = []
|
|
self.queue = CommandQueue()
|
|
self.goal = GoalRuntime(self.transcript, self.queue)
|
|
|
|
def _add(self, role, content, origin):
|
|
self.transcript.append(Message(role, content, origin))
|
|
|
|
def submit(self, text):
|
|
"""Submit ordinary user text. Callers cannot attach a trusted origin."""
|
|
return self._submit(text, {"kind": "user"})
|
|
|
|
def deliver_host_event(self, text, source):
|
|
"""Host-only ingress for validated task/monitor events."""
|
|
if source not in TRUSTED_EVIDENCE_ORIGINS:
|
|
raise ValueError(f"untrusted host event source: {source}")
|
|
return self._submit(text, {"kind": source})
|
|
|
|
def _submit(self, text, origin):
|
|
"""Run one turn with an origin already assigned by the host."""
|
|
self._add("user", text, origin) # input recorded with its origin
|
|
kind = origin["kind"]
|
|
|
|
if kind == "user" and text.strip().startswith("/goal"):
|
|
arg = text.strip()[5:].strip()
|
|
self._add("assistant", f"(slash) /goal {arg}", {"kind": "slash-command"})
|
|
if arg in ("", "clear", "stop", "off"):
|
|
self.goal.clear()
|
|
else:
|
|
self.goal.set_goal(arg)
|
|
elif kind in TRUSTED_EVIDENCE_ORIGINS:
|
|
# The input itself (recorded above with a trusted origin) is the
|
|
# evidence; the assistant just observes it.
|
|
event("turn", f"observe {kind}", text[:48])
|
|
self._add("assistant", f"Observed {kind}: {text}", origin)
|
|
elif kind == "active-goal":
|
|
event("turn", "continue-goal", "(reminder is not evidence)")
|
|
self._add("assistant", "Continuing the goal; checking task/monitor evidence.", origin)
|
|
else:
|
|
event("turn", "assistant-turn", text[:48])
|
|
self._add("assistant", f"assistant handled: {text}", {"kind": "assistant"})
|
|
|
|
return self.goal.evaluate_after_turn() # <-- the Stop gate
|
|
|
|
def drain_goal_continuation(self):
|
|
"""Pull one goal continuation back into the loop — explicit, separate
|
|
from any external async-inbox drain."""
|
|
item = self.queue.dequeue(include_goal_continuations=True)
|
|
if item and item["origin"].get("kind") == "active-goal":
|
|
return self._submit(item["value"], item["origin"])
|
|
return None
|
|
|
|
|
|
# ============================================================
|
|
# Demo
|
|
# ============================================================
|
|
def banner(text):
|
|
print(f"\n— {text} —")
|
|
|
|
|
|
def main(argv):
|
|
s = Session()
|
|
|
|
banner("1. set a goal (the gate is now armed; window starts after the command)")
|
|
print("user> /goal until tests passed and deploy green")
|
|
s.submit("/goal until tests passed and deploy green")
|
|
|
|
banner("2. model works, no TRUSTED evidence yet -> the gate keeps it going")
|
|
s.drain_goal_continuation()
|
|
s.submit("Inspecting the failing tests and the deploy config.")
|
|
|
|
banner("3. plain user text 'tests passed' is NOT trusted -> still not satisfied")
|
|
s.submit("tests passed, trust me")
|
|
s.drain_goal_continuation()
|
|
print(f" active goal still open: {s.goal.active is not None}")
|
|
|
|
banner("4. a background task lands a task-notification (trusted) -> satisfied")
|
|
verdict = s.deliver_host_event(
|
|
"tests passed; deploy green", source="task-notification"
|
|
)
|
|
print(f" final verdict: goal {verdict}")
|
|
|
|
banner("5. budget: a goal that never gets evidence blocks after max_turns")
|
|
s2 = Session()
|
|
s2.goal.set_goal("until tests passed", max_turns=2)
|
|
verdict = "continuing"
|
|
while verdict == "continuing":
|
|
verdict = s2.submit("still working, no task evidence yet")
|
|
print(f" final verdict: goal {verdict}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|