מוסיף web/agent_health.py — מסווג-טהור classify_issue_health (agent-agnostic, testable) שממפה כל issue פתוח+משויך-סוכן למצב-בריאות אחד, ו-get_agent_health ב-paperclip_client שגוזר את הפרימיטיבים מ-heartbeat_runs (run חי=finished_at null בחלון) + agent_wakeup_requests (total + recovery-markers בחלון). מרכיב על אירועי #219 (אותם reasons של recovery). - zombie = פתוח+משויך-סוכן, אין run חי, יש wakeup-recovery → הstranded-child שאובחן ידנית (reference_recovery_loop_stranded_child) עולה אוטומטית. - stalled = ננער חוזר בלי progress; working = run חי; idle = המתנה בנונית. - Port: pc_get_agent_health (read-only, לא עטוף-טלמטריה לפי הכלל); endpoint GET /api/operations/agents/health (worst-first, +counts). - 7 בדיקות pytest (מסווג-טהור + fetch fake-asyncpg). אומת חי read-only מול Paperclip DB: 6 issues→idle נכון, אפס zombie. Invariants: מקיים G12 (מסווג אגנוסטי; fetch מהמעטפת; המגע מהשער), G2 (מקור-בריאות יחיד). UI ב-/operations = follow-up מגודר-שער-עיצוב (feedback_claude_design_gate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
8.1 KiB
Python
189 lines
8.1 KiB
Python
"""Agent Platform Port (INV-G12 / docs/spec/X15) — the single seam to the platform.
|
|
|
|
Paperclip is the *agent platform*: a replaceable shell, not the core. This module
|
|
is the ONLY place in the web layer that may import the Paperclip client
|
|
(``web.paperclip_client`` / ``web.paperclip_api``). ``web/app.py`` — and any future
|
|
web code — imports the platform operations from HERE, never from the Paperclip
|
|
modules directly. So the dependency points inward (Ports & Adapters / the
|
|
Dependency Rule, Cockburn / Martin): swapping the platform means re-implementing
|
|
this one module, not touching app.py.
|
|
|
|
Enforced by the leak-guard (X15 §4 / R4): no file outside this Port imports
|
|
``paperclip_client`` / ``paperclip_api``.
|
|
|
|
Two tiers:
|
|
• **Lifecycle side-effects** — archive / restore / create a case's project, and
|
|
the case-status notification — are also exposed under domain names
|
|
(``archive_case_project`` …). These are the genuine domain events the app
|
|
emits over a case's lifecycle; prefer them in new code.
|
|
• **Issue / interaction / comment / agent operations** are re-exported under
|
|
their existing ``pc_*`` names: they are request/response API calls, not
|
|
events, so a faithful facade re-export — rather than an artificial "event" —
|
|
is the honest treatment. Migrating these to domain verbs is a follow-up that
|
|
can happen as call sites are touched; the import seam (the enforceable part
|
|
of G12) holds regardless.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
# ── the Paperclip shell — imported ONLY here (the Port is the single seam) ──
|
|
from web.paperclip_api import (
|
|
emit_case_status_webhook,
|
|
emit_export_complete_webhook,
|
|
emit_missing_precedent_webhook,
|
|
pc_request,
|
|
require_paperclip_db_url,
|
|
)
|
|
from web.agent_telemetry import instrument
|
|
from web.paperclip_client import (
|
|
COMPANIES as PAPERCLIP_COMPANIES,
|
|
accept_interaction as pc_accept_interaction,
|
|
archive_project as pc_archive_project,
|
|
create_project as pc_create_project,
|
|
create_workflow_issue as pc_create_workflow_issue,
|
|
get_agent_health as pc_get_agent_health,
|
|
get_agents_for_case as pc_get_agents_for_case,
|
|
get_agents_for_company as pc_get_agents,
|
|
get_case_issues as pc_get_case_issues,
|
|
get_issue_comments as pc_get_issue_comments,
|
|
get_issue_interactions as pc_get_issue_interactions,
|
|
get_project_url,
|
|
get_run_events as pc_get_run_events,
|
|
get_run_log as pc_get_run_log,
|
|
list_live_runs as pc_list_live_runs,
|
|
reject_interaction as pc_reject_interaction,
|
|
respond_to_interaction as pc_respond_to_interaction,
|
|
restore_project as pc_restore_project,
|
|
update_project_name as pc_update_project_name,
|
|
get_generation_run_status as pc_get_generation_run_status,
|
|
# ── raw imports: wrapped with telemetry below (state-affecting ops, #219) ──
|
|
cancel_interaction as _cancel_interaction,
|
|
cancel_run as _cancel_run,
|
|
escalate_issue as _escalate_issue,
|
|
post_comment as _post_comment,
|
|
reap_stale_interactions as _reap_stale_interactions,
|
|
reset_agent_session as _reset_agent_session,
|
|
reset_case_agents as _reset_case_agents,
|
|
wake_analyst_for_appraiser_facts as _wake_analyst_for_appraiser_facts,
|
|
wake_analyst_for_argument_aggregation as _wake_analyst_for_argument_aggregation,
|
|
wake_analyst_for_protocol_analysis as _wake_analyst_for_protocol_analysis,
|
|
wake_ceo_agent as _wake_ceo,
|
|
wake_ceo_for_action as _wake_ceo_for_action,
|
|
wake_ceo_for_feedback_fold as _wake_ceo_for_feedback_fold,
|
|
wake_curator_for_final as _wake_curator_for_final,
|
|
wake_for_precedent_extraction as _wake_for_precedent_extraction,
|
|
)
|
|
|
|
# ── telemetry-wrapped platform ops (#219 / docs/spec/X15) ───────────────────
|
|
# Every state-affecting platform op the Port exposes emits one structured event
|
|
# through web.agent_telemetry, so recovery-loop behaviour (repeated wakeups,
|
|
# stranded dispositions) is observable in real time rather than reconstructed
|
|
# from the Paperclip DB. The wrappers preserve the public ``pc_*`` names and
|
|
# signatures — call sites in app.py are unchanged. Read-only observability ops
|
|
# (list_live_runs / get_run_log / get_issue_comments …) are intentionally NOT
|
|
# instrumented: they neither wake agents nor change disposition state.
|
|
pc_wake_ceo = instrument("agent.wakeup", role="ceo")(_wake_ceo)
|
|
pc_wake_ceo_for_action = instrument(
|
|
"agent.wakeup", role="ceo", keys=("case_number", "company_id", "action"),
|
|
)(_wake_ceo_for_action)
|
|
pc_wake_ceo_for_feedback_fold = instrument(
|
|
"agent.wakeup", role="ceo", keys=("feedback_id", "category", "block_id"),
|
|
)(_wake_ceo_for_feedback_fold)
|
|
pc_wake_curator_for_final = instrument(
|
|
"agent.wakeup", role="curator", keys=("case_number", "company_id", "task"),
|
|
)(_wake_curator_for_final)
|
|
pc_wake_for_precedent_extraction = instrument(
|
|
"agent.wakeup", role="ceo", keys=("case_law_id", "citation", "practice_area"),
|
|
)(_wake_for_precedent_extraction)
|
|
pc_wake_analyst_for_appraiser_facts = instrument(
|
|
"agent.wakeup", role="analyst",
|
|
)(_wake_analyst_for_appraiser_facts)
|
|
pc_wake_analyst_for_argument_aggregation = instrument(
|
|
"agent.wakeup", role="analyst",
|
|
)(_wake_analyst_for_argument_aggregation)
|
|
pc_wake_analyst_for_protocol_analysis = instrument(
|
|
"agent.wakeup", role="analyst", keys=("case_number", "company_id", "document_id"),
|
|
)(_wake_analyst_for_protocol_analysis)
|
|
pc_post_comment = instrument(
|
|
"agent.comment", keys=("issue_id", "company_id"),
|
|
)(_post_comment)
|
|
pc_cancel_interaction = instrument(
|
|
"interaction.cancelled", keys=("issue_id", "interaction_id"),
|
|
)(_cancel_interaction)
|
|
# First-class escalation-to-chair (#218): the loop-safe alternative to leaving an
|
|
# issue agent-owned+blocked. Emits a severity-tagged event so escalations are
|
|
# countable per case in the same stream as the wakeups they replace.
|
|
pc_escalate_issue = instrument(
|
|
"agent.escalated", keys=("issue_id", "severity", "company_id", "reason"),
|
|
)(_escalate_issue)
|
|
pc_reap_stale_interactions = instrument(
|
|
"interaction.reaped", result_keys=("cancelled",),
|
|
)(_reap_stale_interactions)
|
|
pc_cancel_run = instrument("run.cancelled", keys=("run_id",))(_cancel_run)
|
|
pc_reset_agent_session = instrument(
|
|
"agent.session_reset", keys=("agent_id",),
|
|
)(_reset_agent_session)
|
|
pc_reset_case_agents = instrument(
|
|
"agent.session_reset", keys=("case_number",),
|
|
)(_reset_case_agents)
|
|
|
|
# ── domain-named lifecycle aliases (preferred for new call sites) ───────────
|
|
archive_case_project = pc_archive_project
|
|
restore_case_project = pc_restore_project
|
|
create_case_project = pc_create_project
|
|
rename_case_project = pc_update_project_name
|
|
notify_case_status = emit_case_status_webhook
|
|
|
|
__all__ = [
|
|
# platform infrastructure
|
|
"PAPERCLIP_COMPANIES",
|
|
"pc_request",
|
|
"require_paperclip_db_url",
|
|
"get_project_url",
|
|
# lifecycle — domain names (preferred) + legacy aliases
|
|
"archive_case_project",
|
|
"restore_case_project",
|
|
"create_case_project",
|
|
"rename_case_project",
|
|
"notify_case_status",
|
|
"emit_case_status_webhook",
|
|
"emit_export_complete_webhook",
|
|
"emit_missing_precedent_webhook",
|
|
"pc_archive_project",
|
|
"pc_restore_project",
|
|
"pc_create_project",
|
|
"pc_update_project_name",
|
|
# issues / workflow
|
|
"pc_create_workflow_issue",
|
|
"pc_get_case_issues",
|
|
# agents / wakeups
|
|
"pc_get_agents_for_case",
|
|
"pc_get_agents",
|
|
"pc_wake_ceo",
|
|
"pc_wake_ceo_for_action",
|
|
"pc_wake_ceo_for_feedback_fold",
|
|
"pc_wake_curator_for_final",
|
|
"pc_wake_for_precedent_extraction",
|
|
"pc_wake_analyst_for_appraiser_facts",
|
|
"pc_wake_analyst_for_argument_aggregation",
|
|
"pc_wake_analyst_for_protocol_analysis",
|
|
"pc_get_generation_run_status",
|
|
# comments / interactions
|
|
"pc_post_comment",
|
|
"pc_get_issue_comments",
|
|
"pc_get_issue_interactions",
|
|
"pc_accept_interaction",
|
|
"pc_reject_interaction",
|
|
"pc_respond_to_interaction",
|
|
"pc_cancel_interaction",
|
|
"pc_reap_stale_interactions",
|
|
"pc_escalate_issue",
|
|
# agent-run observability + control (live view + smart management)
|
|
"pc_get_agent_health",
|
|
"pc_list_live_runs",
|
|
"pc_get_run_log",
|
|
"pc_get_run_events",
|
|
"pc_cancel_run",
|
|
"pc_reset_agent_session",
|
|
"pc_reset_case_agents",
|
|
]
|