feat(token-tracking): add record_interactive_task MCP tool

New tool for capturing ad-hoc work done outside formal workplans.
Finds or creates a persistent 'interactive-<repo>' workstream for the
repo, creates the task, marks it done, and records a token event using
the three-tier logic — all in a single call.

Seeded two example events on interactive-the-custodian:
  - Three-tier token recording on task done (8000/3500)
  - Add record_interactive_task MCP tool (4500/1800)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 18:36:51 +02:00
parent c0eec6e168
commit edffc62775

View File

@@ -2222,6 +2222,113 @@ def get_doi_summary() -> str:
return json.dumps(_get("/repos/doi/summary"), indent=2)
# ---------------------------------------------------------------------------
# Interactive / ad-hoc task recording
# ---------------------------------------------------------------------------
@mcp.tool()
def record_interactive_task(
title: str,
repo_slug: str,
tokens_in: Optional[int] = None,
tokens_out: Optional[int] = None,
model: Optional[str] = None,
agent: Optional[str] = None,
description: Optional[str] = None,
session_id: Optional[str] = None,
) -> str:
"""Record ad-hoc interactive work as a task with token consumption.
Finds or creates a persistent 'interactive-<repo>' workstream for the repo,
creates the task, marks it done immediately, and records a token event using
the three-tier logic (exact > heuristic).
Use this for work done outside a formal workplan: quick fixes, config changes,
code reviews, one-off investigations, or any session work worth tracking.
Args:
title: Short description of the work done
repo_slug: Registered repo slug, e.g. 'the-custodian', 'inter-hub'
tokens_in: Exact input token count for this task (Tier 1 — best practice)
tokens_out: Exact output token count for this task (Tier 1)
model: Model identifier, e.g. 'claude-sonnet-4-6'
agent: Agent name, e.g. 'custodian', 'ralph'
description: Optional longer description of what was done
session_id: Agent session identifier
"""
# Resolve repo
repos = _get("/repos/")
if isinstance(repos, dict) and "error" in repos:
return json.dumps(repos)
repo = next((r for r in (repos or []) if r.get("slug") == repo_slug), None)
if not repo:
return json.dumps({"error": f"Repo not found: {repo_slug!r}. Register it first with register_repo()."})
repo_id = repo["id"]
domain_slug = repo.get("domain_slug") or repo.get("domain")
ws_slug = f"interactive-{repo_slug}"
# Find or create the interactive workstream
existing = _get("/workstreams/", {"slug": ws_slug})
ws = existing[0] if isinstance(existing, list) and existing else None
if not ws:
# Find a topic for this domain to satisfy the FK
topics = _get("/topics/")
topic = next(
(t for t in (topics if isinstance(topics, list) else [])
if t.get("domain_slug") == domain_slug or t.get("domain") == domain_slug),
None,
)
if not topic:
return json.dumps({"error": f"No topic found for domain {domain_slug!r} — cannot create workstream."})
ws = _post("/workstreams", {
"topic_id": topic["id"],
"slug": ws_slug,
"title": f"Interactive — {repo_slug}",
"description": "Ad-hoc tasks created outside a formal workplan.",
"owner": "custodian",
"repo_id": repo_id,
})
if "error" in ws:
return json.dumps(ws)
# Create task
task = _post("/tasks", {
"workstream_id": ws["id"],
"title": title,
"description": description,
"priority": "medium",
})
if "error" in task:
return json.dumps(task)
# Mark done — triggers three-tier token recording in the router
body: dict[str, Any] = {
"status": "done",
"model": model,
"agent": agent,
"session_id": session_id,
}
if tokens_in is not None:
body["tokens_in"] = tokens_in
if tokens_out is not None:
body["tokens_out"] = tokens_out
_patch(f"/tasks/{task['id']}", body)
tier = "exact" if tokens_in is not None else "heuristic"
return json.dumps({
"task_id": task["id"],
"workstream_id": ws["id"],
"workstream_slug": ws_slug,
"title": title,
"token_tier": tier,
}, indent=2)
# ---------------------------------------------------------------------------
# Token events
# ---------------------------------------------------------------------------