feat(agents-tab): הוראה למעלה + פעילות מקופלת + בורר-יעד עם "הרצה חדשה"
טאב הסוכנים בדף-התיק — מימוש מוקאפ X17 מאושר (18i3), פותר את התלונה החוזרת
שהיו"ר נחת בתחתית העמוד, שדה-ההוראה למטה, והטיוטה החדשה נבלעה בגלילה.
Frontend (web-ui/agent-activity-feed.tsx):
- **שדה-ההוראה עלה לראש הטאב** (Composer), מעל הפעילות — הכי זמין, בלי גלילה.
בוטל ה-auto-scroll-to-bottom (endRef/useEffect הוסרו).
- **כל הפעילות מקופלת כברירת-מחדל** — קבוצות-המשימה וגם "ממתין לתשובתך"
(defaultOpen=false); היו"ר פותח כדי להסתכל. סדר החדש-למעלה (תג "החדש למעלה").
- **בורר-יעד** (TargetSelector): מציג לאיזו משימה ההוראה תלך, בוחר משימה פעילה,
מסמן משימות סגורות כמנוטרלות ("שליחה אליהן לא תעיר סוכן"), אזהרת יעד-סגור
עם כפתור-שליחה מנוטרל, ואופציית **"פתח הרצה חדשה"**. מחשב את יעד-ברירת-המחדל
בצד-לקוח במראָה ל-pick_default_comment_target.
Backend:
- `open_ceo_run` (paperclip_client) — פותח הרצת-CEO טרייה דרך פרימיטיב ה-CEO-child
(#227): יוצר issue-ילד בבעלות CEO ומעיר עליו, ועוקף את issue_assignee_changed
על issue בבעלות-אדם (הבאג של #228). נחשף דרך ה-Port כ-`pc_open_ceo_run`.
- `/agents/comment` מקבל `new_run:bool`; `PaperclipIssue` מחזיר `parent_id`
(לבורר בצד-לקוח).
Invariants: G1 (נרמול-במקור), G2 (אין מסלול-ניתוב מקביל), G12 (מגע-Paperclip דרך ה-Port).
מקדם TaskMaster #228 (מנתב הרצה-חדשה דרך פרימיטיב ה-CEO-child).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,7 @@ from web.paperclip_client import (
|
||||
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,
|
||||
open_ceo_run as _open_ceo_run,
|
||||
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,
|
||||
@@ -89,6 +90,9 @@ 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_open_ceo_run = instrument(
|
||||
"agent.wakeup", role="ceo", keys=("case_number", "company_id"),
|
||||
)(_open_ceo_run)
|
||||
pc_wake_ceo_for_feedback_fold = instrument(
|
||||
"agent.wakeup", role="ceo", keys=("feedback_id", "category", "block_id"),
|
||||
)(_wake_ceo_for_feedback_fold)
|
||||
@@ -165,6 +169,7 @@ __all__ = [
|
||||
"pc_get_agents",
|
||||
"pc_wake_ceo",
|
||||
"pc_wake_ceo_for_action",
|
||||
"pc_open_ceo_run",
|
||||
"pc_wake_ceo_for_feedback_fold",
|
||||
"pc_wake_curator_for_final",
|
||||
"pc_wake_for_precedent_extraction",
|
||||
|
||||
30
web/app.py
30
web/app.py
@@ -90,6 +90,7 @@ from web.agent_platform_port import (
|
||||
rename_case_project,
|
||||
pc_wake_ceo,
|
||||
pc_wake_ceo_for_action,
|
||||
pc_open_ceo_run,
|
||||
pc_wake_ceo_for_feedback_fold,
|
||||
pc_wake_curator_for_final,
|
||||
pc_wake_for_precedent_extraction,
|
||||
@@ -4604,15 +4605,38 @@ async def api_case_agents(case_number: str):
|
||||
class AgentCommentRequest(BaseModel):
|
||||
body: str
|
||||
issue_id: str | None = None
|
||||
# "פתח הרצה חדשה" — open a fresh CEO-owned run for this instruction instead of
|
||||
# appending to an existing issue (agents-tab target selector). Overrides issue_id.
|
||||
new_run: bool = False
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/agents/comment")
|
||||
async def api_post_agent_comment(case_number: str, req: AgentCommentRequest):
|
||||
"""Post a comment on a Paperclip issue linked to a case.
|
||||
"""Post a chair instruction to a Paperclip issue linked to a case.
|
||||
|
||||
If ``issue_id`` is omitted, routing prefers the live CEO main issue and
|
||||
never a closed (done/cancelled) issue — see ``_pick_default_comment_target``.
|
||||
Routing (agents-tab target selector):
|
||||
- ``new_run=True`` → open a fresh CEO-owned run (child issue assigned to the
|
||||
CEO + wakeup), sidestepping the ``issue_assignee_changed`` cancellation on
|
||||
a human-owned parent. Overrides ``issue_id``.
|
||||
- ``issue_id`` given → post to that issue (explicit chair choice).
|
||||
- neither → prefer the live CEO main issue, never a closed one
|
||||
(``pick_default_comment_target``).
|
||||
"""
|
||||
if req.new_run:
|
||||
result = await pc_open_ceo_run(case_number, req.body)
|
||||
if result.get("status") != "ok":
|
||||
raise HTTPException(
|
||||
502,
|
||||
f"פתיחת הרצה חדשה נכשלה: {result.get('reason', 'unknown')}",
|
||||
)
|
||||
return {
|
||||
"comment_id": None,
|
||||
"issue_id": result["issue_id"],
|
||||
"issue_identifier": result.get("identifier", ""),
|
||||
"issue_status": "in_progress",
|
||||
"new_run": True,
|
||||
}
|
||||
|
||||
issues = await pc_get_case_issues(case_number)
|
||||
if not issues:
|
||||
raise HTTPException(404, f"לא נמצא פרויקט Paperclip לתיק {case_number}")
|
||||
|
||||
@@ -1739,6 +1739,93 @@ async def wake_ceo_for_action(
|
||||
}
|
||||
|
||||
|
||||
async def open_ceo_run(
|
||||
case_number: str,
|
||||
instruction: str,
|
||||
company_id: str = "",
|
||||
) -> dict:
|
||||
"""Open a FRESH CEO-owned run carrying a free-form chair instruction.
|
||||
|
||||
Drives the "פתח הרצה חדשה" option of the agents-tab target selector: instead
|
||||
of appending the instruction to an existing (possibly human-owned or closed)
|
||||
issue, a **child issue assigned to the CEO** is created under the case's main
|
||||
issue and the CEO is woken on it. This is the same CEO-child primitive as
|
||||
:func:`wake_ceo_for_action` (#227) — it sidesteps the ``issue_assignee_changed``
|
||||
cancellation that fires when an agent is woken on a human-owned parent (the
|
||||
exact silent failure the chair hit on 1027-04-26; see TaskMaster #228).
|
||||
|
||||
Wakeup goes through the Paperclip API, never a direct DB insert. Returns
|
||||
``{"status":"ok", "issue_id"(=child), "identifier", ...}`` or
|
||||
``{"status":"skipped", "reason": ...}``.
|
||||
"""
|
||||
if not PAPERCLIP_BOARD_API_KEY:
|
||||
logger.warning("PAPERCLIP_BOARD_API_KEY not set — skipping CEO new-run")
|
||||
return {"status": "skipped", "reason": "no_api_key"}
|
||||
|
||||
issues = await get_case_issues(case_number)
|
||||
if not issues:
|
||||
logger.warning("No Paperclip issues for case %s — skipping CEO new-run", case_number)
|
||||
return {"status": "skipped", "reason": "no_issue"}
|
||||
|
||||
# Parent the new run under the live main issue (never a closed child).
|
||||
main_issue = pick_default_comment_target(issues)
|
||||
main_issue_id = main_issue["id"]
|
||||
if not company_id:
|
||||
company_id = main_issue.get("company_id", "")
|
||||
ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID)
|
||||
|
||||
title = f'[ערר {case_number}] הוראת יו"ר — הרצה חדשה'
|
||||
description = (
|
||||
'הוראת יו"ר שנשלחה כ"הרצה חדשה" מטאב הסוכנים. קרא את מצב התיק, טפל בהוראה '
|
||||
"בסגנון דפנה, ומסור disposition תקין בסיום.\n\n---\n\n" + instruction
|
||||
)
|
||||
child_resp = await pc_request(
|
||||
"POST",
|
||||
f"/api/issues/{main_issue_id}/children",
|
||||
json={
|
||||
"title": title,
|
||||
"description": description,
|
||||
"status": "in_progress",
|
||||
"priority": "medium",
|
||||
"assigneeAgentId": ceo_id,
|
||||
},
|
||||
raise_on_error=True,
|
||||
)
|
||||
sub_issue = child_resp.json()
|
||||
sub_issue_id = sub_issue["id"]
|
||||
|
||||
try:
|
||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||
try:
|
||||
await _link_case_to_issue(conn, sub_issue_id, case_number)
|
||||
finally:
|
||||
await conn.close()
|
||||
except Exception as e:
|
||||
logger.warning("plugin_state link failed for new-run sub_issue=%s: %s", sub_issue_id, e)
|
||||
|
||||
await pc_request(
|
||||
"POST",
|
||||
f"/api/agents/{ceo_id}/wakeup",
|
||||
json={
|
||||
"source": "on_demand",
|
||||
"triggerDetail": "manual",
|
||||
"reason": f"chair_new_run_{case_number}",
|
||||
"payload": {"issueId": sub_issue_id, "case_number": case_number},
|
||||
},
|
||||
raise_on_error=True,
|
||||
)
|
||||
logger.info(
|
||||
"CEO new-run for case %s: child_issue=%s ceo=%s", case_number, sub_issue_id, ceo_id,
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"issue_id": sub_issue_id,
|
||||
"identifier": sub_issue.get("identifier", ""),
|
||||
"main_issue_id": main_issue_id,
|
||||
"ceo_id": ceo_id,
|
||||
}
|
||||
|
||||
|
||||
# Substring of the generation child-issue title per action (see _ceo_action_brief).
|
||||
GENERATION_TITLE_SUBSTR = {
|
||||
"party_claims_summary": "סיכום-מנהלים",
|
||||
|
||||
Reference in New Issue
Block a user