feat(agents-panel): היררכיה, סמן-כהושלם ידני, ובורר-יעד נגלל (5 תיקוני יו"ר)
טאב הסוכנים בתיק — 5 תיקונים לבקשת היו"ר, מאושרים דרך שער-העיצוב
(Claude Design X17, כרטיס 18i4):
1. היררכיה — הרצה שנפתחה תחת השורש (למשל CMP-220 תחת CMP-189) מסומנת
בתג "↳ <parent>" גם בציר-הזמן וגם בבורר-היעד (בהזחה). הנתון parent_id
כבר הגיע מה-API — תצוגה בלבד.
2. סמן-כהושלם / בטל ידני — כל משימה פתוחה מקבלת פעולות בכותרת. פותר
"אין אפשרות לסמן ידנית" (superseded runs נתקעו ב-in_review בלי מנגנון
סגירה). endpoint חדש: POST /api/cases/{n}/agents/issue-status → דרך
ה-Port (pc_set_issue_status), סגירה loop-safe ישירה ל-DB, בלי wakeup.
3. בורר-היעד נגלל — max-height + overflow פנימי, כך שהתיבה לא בורחת
מגבולות העמוד. פותר "לא רואים את התחתית / לא מגיעים לפתיחת הליך".
4. "פתח הרצה חדשה" עלה לראש הרשימה, מעל "משימות פעילות", ומודגש.
5. קו-הפרדה בין משימות בציר-הזמן עבה יותר (border-b-2 rule).
G12: כל מגע-פלטפורמה עובר דרך agent_platform_port. אימות: lint + tsc +
next build עוברים.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,7 @@ from web.paperclip_client import (
|
||||
reap_stale_interactions as _reap_stale_interactions,
|
||||
reset_agent_session as _reset_agent_session,
|
||||
reset_case_agents as _reset_case_agents,
|
||||
set_issue_status as _set_issue_status,
|
||||
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,
|
||||
@@ -127,6 +128,12 @@ pc_cancel_interaction = instrument(
|
||||
pc_escalate_issue = instrument(
|
||||
"agent.escalated", keys=("issue_id", "severity", "company_id", "reason"),
|
||||
)(_escalate_issue)
|
||||
# Manual chair close of a board issue (done/cancelled) — the loop-safe primitive
|
||||
# behind the agents-board "סמן כהושלם / בטל" actions. Countable per case in the
|
||||
# same telemetry stream as the escalations/wakeups it sits beside.
|
||||
pc_set_issue_status = instrument(
|
||||
"issue.status_set", keys=("issue_id", "status", "company_id"),
|
||||
)(_set_issue_status)
|
||||
pc_reap_stale_interactions = instrument(
|
||||
"interaction.reaped", result_keys=("cancelled",),
|
||||
)(_reap_stale_interactions)
|
||||
@@ -192,6 +199,7 @@ __all__ = [
|
||||
"pc_cancel_interaction",
|
||||
"pc_reap_stale_interactions",
|
||||
"pc_escalate_issue",
|
||||
"pc_set_issue_status",
|
||||
# agent-run observability + control (live view + smart management)
|
||||
"pc_get_agent_health",
|
||||
"pc_get_recent_escalations",
|
||||
|
||||
24
web/app.py
24
web/app.py
@@ -82,6 +82,7 @@ from web.agent_platform_port import (
|
||||
pc_reset_agent_session,
|
||||
pc_reset_case_agents,
|
||||
pc_respond_to_interaction,
|
||||
pc_set_issue_status,
|
||||
pc_restore_project,
|
||||
pc_wake_analyst_for_appraiser_facts,
|
||||
pc_wake_analyst_for_argument_aggregation,
|
||||
@@ -4851,6 +4852,29 @@ async def api_escalate_issue(case_number: str, req: EscalateRequest):
|
||||
return result
|
||||
|
||||
|
||||
class IssueStatusRequest(BaseModel):
|
||||
issue_id: str
|
||||
status: Literal["done", "cancelled"]
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/agents/issue-status")
|
||||
async def api_set_issue_status(case_number: str, req: IssueStatusRequest):
|
||||
"""Manually close a board issue to done/cancelled from the chair UI.
|
||||
|
||||
Lets the chair tidy the agents board by hand — superseded "הרצה חדשה" runs
|
||||
left in ``in_review`` had no manual close control and piled up. Only the two
|
||||
terminal statuses are accepted; the loop-safe direct-DB close lives behind the
|
||||
Port (``pc_set_issue_status``) and issues no wakeup.
|
||||
"""
|
||||
issues = await pc_get_case_issues(case_number)
|
||||
if not any(i["id"] == req.issue_id for i in issues):
|
||||
raise HTTPException(404, f"Issue {req.issue_id} לא שייך לתיק {case_number}")
|
||||
result = await pc_set_issue_status(req.issue_id, req.status)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(400, result.get("error", "עדכון הסטטוס נכשל"))
|
||||
return result
|
||||
|
||||
|
||||
# ── Settings: MCP Server Configuration ────────────────────────────
|
||||
#
|
||||
# Source of truth for legal-ai env vars is Coolify (see memory:
|
||||
|
||||
@@ -988,6 +988,70 @@ async def escalate_issue(
|
||||
}
|
||||
|
||||
|
||||
async def set_issue_status(
|
||||
issue_id: str, status: str, company_id: str = "",
|
||||
) -> dict:
|
||||
"""Manually close a case issue to ``done`` or ``cancelled`` from the chair UI.
|
||||
|
||||
The chair needs to tidy the agents board by hand: superseded "הרצה חדשה" runs
|
||||
that were replaced by a later run pile up in ``in_review`` with no way to close
|
||||
them — there was no manual control, so they had to be closed out-of-band. This
|
||||
is the loop-safe primitive behind the board's "סמן כהושלם / בטל" actions.
|
||||
|
||||
Only the two **terminal** statuses are accepted (``CLOSED_ISSUE_STATUSES``) —
|
||||
this tidies the board, it does not drive the workflow (agents own the
|
||||
todo/in_progress/in_review transitions). Direct-DB in one transaction,
|
||||
mirroring :func:`escalate_issue`: bypasses Paperclip's disposition resolver
|
||||
(whose multi-PATCH/``issue.released`` behaviour *causes* the recovery loops),
|
||||
sets the matching close timestamp, clears any agent assignee so no recovery
|
||||
sweep re-acts on it, and records an ``author_type='system'`` audit note (inert
|
||||
w.r.t. the user-comment routing sweep, so it will not wake an agent). No wakeup.
|
||||
"""
|
||||
if status not in CLOSED_ISSUE_STATUSES:
|
||||
return {
|
||||
"ok": False,
|
||||
"error": f"invalid status {status!r}; expected one of {tuple(CLOSED_ISSUE_STATUSES)}",
|
||||
}
|
||||
|
||||
# status is validated against the frozenset above, so the interpolated column
|
||||
# name is one of exactly two literals — no injection surface.
|
||||
ts_col = "completed_at" if status == "done" else "cancelled_at"
|
||||
note = (
|
||||
"✓ סומן ידנית כהושלם ע\"י היו\"ר"
|
||||
if status == "done"
|
||||
else "✕ בוטל ידנית ע\"י היו\"ר"
|
||||
)
|
||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||
try:
|
||||
async with conn.transaction():
|
||||
row = await conn.fetchrow(
|
||||
f"""UPDATE issues
|
||||
SET status=$1, {ts_col}=now(),
|
||||
assignee_agent_id=null, updated_at=now()
|
||||
WHERE id=$2::uuid
|
||||
RETURNING id, identifier, company_id""",
|
||||
status, issue_id,
|
||||
)
|
||||
if not row:
|
||||
return {"ok": False, "error": f"issue {issue_id} not found"}
|
||||
cid = company_id or str(row["company_id"])
|
||||
await conn.execute(
|
||||
"""INSERT INTO issue_comments (id, company_id, issue_id, body, author_type)
|
||||
VALUES ($1::uuid, $2::uuid, $3::uuid, $4, 'system')""",
|
||||
str(uuid.uuid4()), cid, issue_id, note,
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
logger.info("Chair set issue %s → %s", issue_id, status)
|
||||
return {
|
||||
"ok": True,
|
||||
"id": str(row["id"]),
|
||||
"identifier": row["identifier"],
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
async def respond_to_interaction(
|
||||
issue_id: str, interaction_id: str, payload: dict,
|
||||
) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user