Skip to content

Commit f63701b

Browse files
authored
Merge pull request #173 from latchbio/aidan/combine-signals-with-cells
Agent Context -- Combine Signals Into Cells
2 parents b3ac4ea + 4f2b2a4 commit f63701b

File tree

7 files changed

+136
-132
lines changed

7 files changed

+136
-132
lines changed

runtime/mount/agent.py

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
cache_chunk_size = 20
2828

29+
reactivity_ready_statuses = {"ran", "ok", "success", "error"}
30+
2931
sandbox_root = os.environ.get("LATCH_SANDBOX_ROOT")
3032
if sandbox_root:
3133
import pathlib
@@ -2504,8 +2506,11 @@ async def get_global_info(args: dict) -> dict:
25042506
})
25052507
self.tool_map["get_global_info"] = get_global_info
25062508

2507-
async def refresh_cells_context(args: dict) -> dict:
2508-
context_result = await self.atomic_operation("get_context")
2509+
async def refresh_cells_context(_args: dict) -> dict:
2510+
context_result, reactivity_result = await asyncio.gather(
2511+
self.atomic_operation("get_context"),
2512+
self.atomic_operation("request_reactivity_summary"),
2513+
)
25092514

25102515
if context_result.get("status") != "success":
25112516
return {
@@ -2514,6 +2519,14 @@ async def refresh_cells_context(args: dict) -> dict:
25142519
"summary": f"Failed to refresh cells: {context_result.get('error', 'Unknown error')}"
25152520
}
25162521

2522+
if reactivity_result.get("status") != "success":
2523+
print(f"[tool] refresh_cells_context: failed to get reactivity summary: {reactivity_result.get('error', 'Unknown error')}")
2524+
return {
2525+
"tool_name": "refresh_cells_context",
2526+
"success": False,
2527+
"summary": f"Failed to get reactivity summary: {reactivity_result.get('error', 'Unknown error')}"
2528+
}
2529+
25172530
context = context_result.get("context", {})
25182531
self.latest_notebook_context = context
25192532

@@ -2533,6 +2546,8 @@ async def refresh_cells_context(args: dict) -> dict:
25332546

25342547
current_tab_name = default_tab_name
25352548

2549+
cell_reactivity: dict[str, dict[str, object]] = reactivity_result.get("cell_reactivity", {})
2550+
25362551
for cell in cells:
25372552
index = cell.get("index", "?")
25382553
cell_id = cell.get("cell_id", "?")
@@ -2578,65 +2593,61 @@ async def refresh_cells_context(args: dict) -> dict:
25782593
w_label = w.get("label", "")
25792594
cell_lines.append(f"- WIDGET: {w_type} | {w_label} | {w_key}")
25802595

2581-
context_dir = context_root / "notebook_context"
2582-
context_dir.mkdir(parents=True, exist_ok=True)
2583-
(context_dir / "cells.md").write_text("\n".join(cell_lines))
2596+
if tf_id is None:
2597+
continue
25842598

2585-
return {
2586-
"tool_name": "refresh_cells_context",
2587-
"success": True,
2588-
"summary": f"Refreshed cells context for {cell_count} cells and stored result in notebook_context/cells.md",
2589-
"cell_count": cell_count,
2590-
"context_path": "notebook_context/cells.md"
2591-
}
2599+
reactivity_meta = cell_reactivity.get(str(tf_id))
2600+
is_reactivity_ready = status in reactivity_ready_statuses
25922601

2593-
async def refresh_reactivity_context(args: dict) -> dict:
2594-
reactivity_result = await self.atomic_operation("request_reactivity_summary")
2602+
cell_lines.append("\nREACTIVITY:")
25952603

2596-
if reactivity_result.get("status") != "success":
2597-
return {
2598-
"tool_name": "refresh_reactivity_context",
2599-
"success": False,
2600-
"summary": f"Failed to refresh reactivity: {reactivity_result.get('error', 'Unknown error')}"
2601-
}
2604+
if not is_reactivity_ready:
2605+
cell_lines.append("- Not available: run this cell to establish reactive dependencies.")
2606+
continue
2607+
2608+
if reactivity_meta is None:
2609+
cell_lines.append("- Reactivity data missing for this cell.")
2610+
continue
26022611

2603-
reactivity_summary = reactivity_result.get("summary")
2612+
signals_defined = reactivity_meta.get("signals_defined", [])
2613+
depends_on_signals = reactivity_meta.get("depends_on_signals", [])
2614+
depends_on_cells = reactivity_meta.get("depends_on_cells", [])
2615+
2616+
cell_lines.append( # noqa: FURB113
2617+
"- Signals defined: "
2618+
+ (", ".join(signals_defined) if signals_defined else "None")
2619+
)
2620+
cell_lines.append(
2621+
"- Depends on signals: "
2622+
+ (", ".join(depends_on_signals) if depends_on_signals else "None")
2623+
)
2624+
cell_lines.append(
2625+
"- Depends on cells: "
2626+
+ (", ".join(depends_on_cells) if depends_on_cells else "None")
2627+
)
26042628

26052629
context_dir = context_root / "notebook_context"
26062630
context_dir.mkdir(parents=True, exist_ok=True)
2607-
2608-
if reactivity_summary is not None:
2609-
(context_dir / "signals.md").write_text(reactivity_summary)
2610-
else:
2611-
(context_dir / "signals.md").write_text("# Reactivity Summary\n\nNo reactive dependencies in this notebook.\n")
2631+
(context_dir / "cells.md").write_text("\n".join(cell_lines))
26122632

26132633
return {
2614-
"tool_name": "refresh_reactivity_context",
2634+
"tool_name": "refresh_cells_context",
26152635
"success": True,
2616-
"summary": "Refreshed reactivity context and stored result in notebook_context/signals.md",
2617-
"context_path": "notebook_context/signals.md",
2636+
"summary": f"Refreshed reactive notebook context for {cell_count} cells and stored result in notebook_context/cells.md",
2637+
"cell_count": cell_count,
2638+
"context_path": "notebook_context/cells.md"
26182639
}
26192640

26202641
self.tools.append({
26212642
"name": "refresh_cells_context",
2622-
"description": "Refresh the cells.md context file with current notebook cell structure and contents.",
2643+
"description": "Refresh the cells.md context file with current reactive notebook structure and contents.",
26232644
"input_schema": {
26242645
"type": "object",
26252646
"properties": {},
26262647
},
26272648
})
26282649
self.tool_map["refresh_cells_context"] = refresh_cells_context
26292650

2630-
self.tools.append({
2631-
"name": "refresh_reactivity_context",
2632-
"description": "Refresh the signals.md context file with current reactive signal dependencies.",
2633-
"input_schema": {
2634-
"type": "object",
2635-
"properties": {},
2636-
},
2637-
})
2638-
self.tool_map["refresh_reactivity_context"] = refresh_reactivity_context
2639-
26402651
if len(self.tools) > 0:
26412652
self.tools[-1]["cache_control"] = {"type": "ephemeral"}
26422653

@@ -3198,7 +3209,6 @@ def read_context_file(filename: str) -> str:
31983209
return f"# {filename}\n\nFile not yet generated."
31993210

32003211
cells_content = read_context_file("cells.md")
3201-
signals_content = read_context_file("signals.md")
32023212

32033213
def build_tree(path: Path, prefix: str = "") -> list[str]:
32043214
lines = []
@@ -3224,7 +3234,6 @@ def build_tree(path: Path, prefix: str = "") -> list[str]:
32243234
"truncated_messages": truncated_messages,
32253235
"model": self.mode_config.get(self.mode, ("claude-sonnet-4-5-20250929", 1024))[0],
32263236
"cells": cells_content,
3227-
"signals": signals_content,
32283237
"tree": tree_content,
32293238
}
32303239

runtime/mount/agent_config/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@ agent_config/
2323
│ │ ├── reactivity.mdx
2424
│ │ └── widget-types.mdx
2525
│ ├── agent_scratch/ # Agent-created notes and state
26-
│ └── notebook_context/ # Auto-generated runtime state
27-
│ ├── cells.md # (populated by the agent calling tool to refresh the context)
28-
│ ├── globals.md # (populated by the agent calling tool to refresh the context)
29-
│ └── signals.md # (populated by the agent calling tool to refresh the context)
26+
│ └── notebook_context/
27+
│ └── cells.md # Populated by the agent via refresh_cells_context
3028
└── README.md # This file
3129
```
3230

runtime/mount/agent_config/system_prompt.md

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -726,10 +726,9 @@ See reactivity documentation in the `## Reactivity` section of `latch_api_docs/l
726726
727727
### Context Refresh Tools
728728
729-
- `refresh_cells_context` - Update cells.md with current notebook structure
730-
- `refresh_reactivity_context` - Update signals.md with signal dependencies
729+
- `refresh_cells_context` - Update `cells.md` with current reactive notebook structure
731730
732-
Use these tools to refresh notebook state files before reading them.
731+
Use this tool to refresh `cells.md` with the current reactive notebook state before reading it.
733732
734733
### Introspection Tools
735734
@@ -744,7 +743,7 @@ Use these tools to refresh notebook state files before reading them.
744743
745744
## Context Files (Refresh On-Demand)
746745
747-
The notebook state is available in two context files. These files are initialized when you start but are NOT automatically updated - you must explicitly refresh them when needed.
746+
The reactive notebook state is persisted in `cells.md`. You must explicitly refresh it when needed.
748747
749748
### cells.md
750749
@@ -756,37 +755,20 @@ The notebook state is available in two context files. These files are initialize
756755
- After creating cells to see updated structure
757756
- Looking for specific code or widget locations
758757
- Checking cell execution status
758+
- Creating or reasoning about reactive relationships between cells
759759
760760
**Refresh tool:** `refresh_cells_context`
761761
762762
- Returns updated cell count
763763
- Returns context path to read result from
764-
- Writes latest cell structure to cells.md
764+
- Writes latest cell structure and, for every cell, the signals it defines and the signals/cells it depends on
765765
766766
**Search with grep:**
767767
768768
- Find by ID: `grep "CELL_ID: abc123" notebook_context/cells.md`
769769
- Find by code: `grep "import pandas" notebook_context/cells.md`
770770
771-
**Format:** Cell metadata on separate lines (CELL_ID, CELL_INDEX, TYPE, STATUS), code between CODE_START/CODE_END markers.
772-
773-
### signals.md
774-
775-
**Location:** `notebook_context/signals.md`
776-
777-
**Refresh when:**
778-
779-
- Designing reactive workflows
780-
- Debugging reactivity issues
781-
- Before creating cross-cell dependencies
782-
783-
**Refresh tool:** `refresh_reactivity_context`
784-
785-
- Returns success status
786-
- Returns context path to read result from
787-
- Writes latest reactivity graph to signals.md
788-
789-
**Contents:** Signal dependencies between cells, widget signals, global variable signals, subscription relationships.
771+
**Format:** Cell metadata on separate lines (CELL_ID, CELL_INDEX, TYPE, STATUS), code between `CODE_START/CODE_END` markers, followed by a `REACTIVITY` subsection summarizing which reactive signals this cell defines along with the signals and cells it depends on (will trigger this cell to re-run).
790772
791773
### Refresh Strategy
792774
@@ -795,7 +777,6 @@ The notebook state is available in two context files. These files are initialize
795777
**Refresh selectively:**
796778
797779
- Refresh cells before inspecting/modifying notebook structure
798-
- Refresh reactivity when working with signals
799780
800781
### Creating Custom Files
801782
@@ -1223,7 +1204,7 @@ Which result would you like to proceed with?""",
12231204
10. **Plots MUST render via `w_plot`** - Every figure requires the plot widget
12241205
11. **Transformation cells MUST be self-contained** - Include all imports, definitions, and variable creation
12251206
12. **Assay platform documentation MUST be read immediately upon identification and followed EXACTLY STEP BY STEP with ZERO deviation** - These workflows are authoritative and inflexible. Every action must be verified against the current step. Manual alternatives are forbidden when workflows are specified.
1226-
13. **Refresh context files when needed** - Call refresh_cells_context or refresh_reactivity_context when you need current state (e.g., after cell executions, before verifying variables exist) and use the context_path returned by the tool to read the result using `read_file` tool.
1207+
13. **Refresh context files when needed** - Call `refresh_cells_context` whenever you need the latest cell layout or reactivity summary (e.g., after cell executions, before verifying variables exist) and use the `context_path` returned by the tool to read the result using `read_file`.
12271208
14. **Widget keys cannot be assumed** - If you are creating widget(s) and need the widget key(s), call refresh_cells_context after the cell with the widget(s) has run
12281209

12291210
## NEVER Do

runtime/mount/entrypoint.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ async def handle_kernel_messages(conn_k: SocketIo, auth: str) -> None:
434434
"type": "agent_action_response",
435435
"tx_id": tx_id,
436436
"status": "success",
437-
"summary": msg.get("summary", "")
437+
"cell_reactivity": msg.get("cell_reactivity", {}),
438438
})
439439
continue
440440

@@ -481,7 +481,9 @@ async def handle_agent_messages(conn_a: SocketIo) -> None:
481481
while True:
482482
msg = await conn_a.recv()
483483
msg_type = msg.get("type", "unknown")
484-
print(f"[entrypoint] Agent > {msg_type}")
484+
485+
if msg_type != "agent_stream_delta":
486+
print(f"[entrypoint] Agent > {msg_type}")
485487

486488
if msg_type == "agent_action" and msg.get("action") == "request_reactivity_summary":
487489
if k_proc.conn_k is not None:

0 commit comments

Comments
 (0)