Commit ebbc162399232750edcd562d7a6ee83c3e8cde80
1 parent
98d86e47
graphlemon: auto-commit uncommitted changes for judgment-92eccdb1933c-main-checkout-drift
Showing
2 changed files
with
75 additions
and
1 deletions
src/task_query.py
| @@ -206,9 +206,10 @@ def get_task(conn, task_id): | @@ -206,9 +206,10 @@ def get_task(conn, task_id): | ||
| 206 | task["work_graph_runs"] = [] | 206 | task["work_graph_runs"] = [] |
| 207 | for w in runs: | 207 | for w in runs: |
| 208 | nodes = _work_nodes_for_run(conn, w[0]) | 208 | nodes = _work_nodes_for_run(conn, w[0]) |
| 209 | + graph_manifest = json.loads(w[1]) if w[1] else None | ||
| 209 | task["work_graph_runs"].append({ | 210 | task["work_graph_runs"].append({ |
| 210 | "run_id": w[0], | 211 | "run_id": w[0], |
| 211 | - "graph_manifest": json.loads(w[1]) if w[1] else None, | 212 | + "graph_manifest": _display_graph_manifest(graph_manifest, nodes), |
| 212 | "status": w[2], "started_at": w[3], "completed_at": w[4], | 213 | "status": w[2], "started_at": w[3], "completed_at": w[4], |
| 213 | # D (2026-08-06): both None for a normal run; both set when this | 214 | # D (2026-08-06): both None for a normal run; both set when this |
| 214 | # run was forked from a historical node (src/work_graph_fork.py). | 215 | # run was forked from a historical node (src/work_graph_fork.py). |
src/work_graph_plan.py
| @@ -29,6 +29,13 @@ import json | @@ -29,6 +29,13 @@ import json | ||
| 29 | import subprocess | 29 | import subprocess |
| 30 | import uuid | 30 | import uuid |
| 31 | 31 | ||
| 32 | +from src import execution_log, paths | ||
| 33 | + | ||
| 34 | +# node_id reserved for the synthetic work_node_execution _record_planning_node | ||
| 35 | +# creates below -- task_query.py matches on this exact string to recognize | ||
| 36 | +# "this run has a real planning node to display", so keep the two in sync. | ||
| 37 | +PLANNING_NODE_ID = "__planning__" | ||
| 38 | + | ||
| 32 | GRAPH_MANIFEST_SCHEMA = { | 39 | GRAPH_MANIFEST_SCHEMA = { |
| 33 | "type": "object", | 40 | "type": "object", |
| 34 | "required": ["nodes", "edges"], | 41 | "required": ["nodes", "edges"], |
| @@ -571,6 +578,71 @@ def _normalize_graph_manifest(manifest, additional_valid_loop_back_target_ids=() | @@ -571,6 +578,71 @@ def _normalize_graph_manifest(manifest, additional_valid_loop_back_target_ids=() | ||
| 571 | return manifest | 578 | return manifest |
| 572 | 579 | ||
| 573 | 580 | ||
| 581 | +def _record_planning_node(conn, run_id, task_id, org_node, prompt, raw_response, graph_manifest): | ||
| 582 | + """Persists the planning call itself (the `claude --agent <org_node> -p | ||
| 583 | + <prompt>` subprocess call in plan_work_graph() below, NOT any node it | ||
| 584 | + planned) as a real, clickable work_node_execution -- previously this | ||
| 585 | + call's own raw output was discarded the moment _extract_graph_manifest | ||
| 586 | + pulled the JSON back out of it, and the call itself left no trace | ||
| 587 | + anywhere the console renders (v_task_detail's to-do tree/DAG only ever | ||
| 588 | + show the nodes a planning call PRODUCED, never the act of planning | ||
| 589 | + itself). Deliberately does NOT touch work_graph_run.graph_manifest -- | ||
| 590 | + that column stays the pure, LLM-authored manifest run_graph()/fork/ | ||
| 591 | + append-mode all still execute and merge against unchanged; task_query.py | ||
| 592 | + synthesizes a display-only node from THIS row (matched by node_id == | ||
| 593 | + PLANNING_NODE_ID) when building the console's API response instead, so | ||
| 594 | + execution/fork/append logic never has to know this row exists. | ||
| 595 | + | ||
| 596 | + Only called from the fresh (non-append) branch of plan_work_graph() -- | ||
| 597 | + one real "how was this run planned" entry per run is enough to answer | ||
| 598 | + the question; Coder/Validator's own append_to_run_id planning calls | ||
| 599 | + aren't given one (YAGNI until someone actually needs to inspect those | ||
| 600 | + too).""" | ||
| 601 | + project_id = conn.execute("SELECT project_id FROM task WHERE task_id = ?", (task_id,)).fetchone()[0] | ||
| 602 | + | ||
| 603 | + # work_node_execution.skill_ref is a required FK to a real skill row. | ||
| 604 | + # design-dynamic-work-graph is the actual Skill documenting this | ||
| 605 | + # self-planning mechanism (skills/design-dynamic-work-graph/SKILL.md) -- | ||
| 606 | + # reused here rather than inventing a placeholder skill_id nothing else | ||
| 607 | + # would ever reference. | ||
| 608 | + skill_id = "design-dynamic-work-graph" | ||
| 609 | + if conn.execute("SELECT 1 FROM skill WHERE skill_id = ?", (skill_id,)).fetchone() is None: | ||
| 610 | + conn.execute( | ||
| 611 | + "INSERT INTO skill (skill_id, owner_org_node, file_path, current_git_commit_sha, source, status) " | ||
| 612 | + "VALUES (?, ?, ?, 'unknown', 'new', 'active')", | ||
| 613 | + (skill_id, org_node, "skills/" + skill_id + "/"), | ||
| 614 | + ) | ||
| 615 | + | ||
| 616 | + node_exec_id = str(uuid.uuid4()) | ||
| 617 | + conn.execute( | ||
| 618 | + "INSERT INTO work_node_execution (node_exec_id, run_id, node_id, subagent_type, skill_ref, " | ||
| 619 | + "input_schema_ref, output_schema_ref, input_payload, output_payload, status) " | ||
| 620 | + "VALUES (?, ?, ?, ?, ?, 'work_graph_plan.goal_text', 'work_graph_plan.graph_manifest', ?, ?, 'done')", | ||
| 621 | + ( | ||
| 622 | + node_exec_id, run_id, PLANNING_NODE_ID, org_node, skill_id, | ||
| 623 | + json.dumps({"prompt": prompt}), json.dumps({"graph_manifest": graph_manifest}), | ||
| 624 | + ), | ||
| 625 | + ) | ||
| 626 | + | ||
| 627 | + log_file_id = str(uuid.uuid4()) | ||
| 628 | + log_path = paths.log_file_path(project_id, task_id, run_id, node_exec_id) | ||
| 629 | + conn.execute( | ||
| 630 | + "INSERT INTO execution_log_file (log_file_id, node_exec_id, project_id, file_path, format, status) " | ||
| 631 | + "VALUES (?, ?, ?, ?, 'jsonl', 'closed')", | ||
| 632 | + (log_file_id, node_exec_id, project_id, log_path), | ||
| 633 | + ) | ||
| 634 | + # execution_log.append_log does its own conn.commit() -- covers the two | ||
| 635 | + # INSERTs above too, same connection/transaction. | ||
| 636 | + execution_log.append_log( | ||
| 637 | + conn, node_exec_id, | ||
| 638 | + json.dumps({"event": "node_start", "org_node": org_node, "prompt": prompt}, ensure_ascii=False) + "\n" | ||
| 639 | + + json.dumps( | ||
| 640 | + {"event": "node_completed", "raw_response": raw_response, "graph_manifest": graph_manifest}, | ||
| 641 | + ensure_ascii=False, | ||
| 642 | + ) + "\n", | ||
| 643 | + ) | ||
| 644 | + | ||
| 645 | + | ||
| 574 | def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, timeout=300, append_to_run_id=None): | 646 | def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, timeout=300, append_to_run_id=None): |
| 575 | """Returns {"run_id", "graph_manifest", "new_node_ids"}. Creates one | 647 | """Returns {"run_id", "graph_manifest", "new_node_ids"}. Creates one |
| 576 | `work_graph_run` row (status='planning') -- moving it to 'running'/ | 648 | `work_graph_run` row (status='planning') -- moving it to 'running'/ |
| @@ -672,6 +744,7 @@ def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, time | @@ -672,6 +744,7 @@ def plan_work_graph(conn, org_node, task_id, goal_text, target_folder_path, time | ||
| 672 | (run_id, task_id, json.dumps(new_manifest)), | 744 | (run_id, task_id, json.dumps(new_manifest)), |
| 673 | ) | 745 | ) |
| 674 | conn.commit() | 746 | conn.commit() |
| 747 | + _record_planning_node(conn, run_id, task_id, org_node, prompt, payload.get("result", ""), new_manifest) | ||
| 675 | return {"run_id": run_id, "graph_manifest": new_manifest, "new_node_ids": []} | 748 | return {"run_id": run_id, "graph_manifest": new_manifest, "new_node_ids": []} |
| 676 | 749 | ||
| 677 | new_node_ids = [n["node_id"] for n in new_manifest["nodes"]] | 750 | new_node_ids = [n["node_id"] for n in new_manifest["nodes"]] |