Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ jobs:
python-version: "3.10"

- name: Install ruff
run: pip install ruff
# Pinned so CI never floats to a new ruff release mid-review. Keep in
# sync with the ruff-pre-commit rev in .pre-commit-config.yaml and the
# pin in pyproject.toml.
run: pip install ruff==0.16.0

- name: ruff check
run: ruff check .
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.17 # run `pre-commit autoupdate` to pin to latest
rev: v0.16.0 # keep in sync with the ruff pins in pyproject.toml + lint.yml; bump via `pre-commit autoupdate`
hooks:
- id: ruff
args: [--fix]
Expand Down
75 changes: 44 additions & 31 deletions docs/HIERARCHICAL_CONTEXTS_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,27 +106,27 @@ class ContextScope:
scope_id: str
parent_id: Optional[str]
scope_type: Literal["root", "focus", "timelapse", "embryo_perception", "ad_hoc"]
purpose: str # one-line task description
instructions: str # full brief, like a prompt to a colleague
purpose: str # one-line task description
instructions: str # full brief, like a prompt to a colleague
conversation_history: List[Dict]
allowed_tools: Set[str]
model: str # "opus" | "sonnet" | "haiku"
model: str # "opus" | "sonnet" | "haiku"
status: Literal["active", "completed", "yielded", "cancelled", "failed"]
summary: Optional[str] # produced by Summarizer at end
key_findings: Dict[str, Any] # structured return value
children: List[str] # child scope_ids
summary: Optional[str] # produced by Summarizer at end
key_findings: Dict[str, Any] # structured return value
children: List[str] # child scope_ids
created_at: datetime
completed_at: Optional[datetime]
persist: bool # write to SQLite if True
persist: bool # write to SQLite if True

# Runtime
inbox: asyncio.Queue # incoming messages from other scopes
wakeup: asyncio.Event # signaled when there is work
inbox: asyncio.Queue # incoming messages from other scopes
wakeup: asyncio.Event # signaled when there is work
cancel_token: asyncio.Event
cancel_reason: Optional[str]
pending_queries: Dict[str, asyncio.Future]
peer_events: List[str] # event types this scope subscribes to
parent_snapshot_cache: Dict # parent's last known summary + findings
peer_events: List[str] # event types this scope subscribes to
parent_snapshot_cache: Dict # parent's last known summary + findings
```

A `ScopeManager` (lives on `MicroscopyCopilot`) owns the scope tree, dispatches the inner agent loop, and exposes the orchestrator-facing tools.
Expand Down Expand Up @@ -198,22 +198,26 @@ async def run_scope(scope: ContextScope) -> SummaryResult:
while not scope.inbox.empty():
items.append(scope.inbox.get_nowait())
if not items and not scope.cancel_token.is_set():
continue # nothing to do, back to sleep, no API call
continue # nothing to do, back to sleep, no API call

# CHECK CANCELLATION — give child one final turn to summarize
if scope.cancel_token.is_set() and scope.status != "cancelling":
scope.status = "cancelling"
scope.conversation_history.append({
"role": "user",
"content": f"[CANCELLATION] {scope.cancel_reason}. "
f"Emit a final summary using `complete` and stop.",
})
scope.conversation_history.append(
{
"role": "user",
"content": f"[CANCELLATION] {scope.cancel_reason}. "
f"Emit a final summary using `complete` and stop.",
}
)

# APPEND injected messages to history
scope.conversation_history.append({
"role": "user",
"content": format_inbox_items(items),
})
scope.conversation_history.append(
{
"role": "user",
"content": format_inbox_items(items),
}
)

# ONE LLM TURN
response = await call_claude(
Expand All @@ -222,23 +226,32 @@ async def run_scope(scope: ContextScope) -> SummaryResult:
messages=scope.conversation_history,
tools=tool_registry.filter(scope.allowed_tools),
)
scope.conversation_history.append({"role":"assistant","content":response})
scope.conversation_history.append({"role": "assistant", "content": response})

# HANDLE TOOL CALLS
if response.stop_reason == "tool_use":
tool_results = []
for tool_call in response.tool_uses:
match tool_call.name:
case "delegate": result = await spawn_and_run(scope, **tool_call.input)
case "yield_checkpoint": result = await emit_checkpoint(scope, **tool_call.input)
case "escalate": result = await escalate_to_root(scope, **tool_call.input)
case "query_parent": result = await ask_parent(scope, **tool_call.input, timeout=30)
case "read_parent_state": result = scope.parent_snapshot_cache
case "respond_to_query": result = await deliver_answer(**tool_call.input)
case "complete": scope.status = "completed"; scope.key_findings = tool_call.input
case _: result = await execute_tool(tool_call, scope)
case "delegate":
result = await spawn_and_run(scope, **tool_call.input)
case "yield_checkpoint":
result = await emit_checkpoint(scope, **tool_call.input)
case "escalate":
result = await escalate_to_root(scope, **tool_call.input)
case "query_parent":
result = await ask_parent(scope, **tool_call.input, timeout=30)
case "read_parent_state":
result = scope.parent_snapshot_cache
case "respond_to_query":
result = await deliver_answer(**tool_call.input)
case "complete":
scope.status = "completed"
scope.key_findings = tool_call.input
case _:
result = await execute_tool(tool_call, scope)
tool_results.append(result)
scope.conversation_history.append({"role":"user","content":tool_results})
scope.conversation_history.append({"role": "user", "content": tool_results})

if response.stop_reason == "end_turn":
scope.status = "completed"
Expand Down
73 changes: 44 additions & 29 deletions docs/PERCEPTION_V2_IMPROVEMENT_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ MISSING: hatching/ folder - NO HATCHING EXAMPLES!
```python
def at_least_stage(self, stage: str) -> bool:
"""Check if embryo has reached at least the given stage."""
order = ['early', 'bean', 'comma', '1.5fold', '2fold', '3fold', 'hatching', 'hatched']
order = ["early", "bean", "comma", "1.5fold", "2fold", "3fold", "hatching", "hatched"]
# ... simple linear comparison
```

Expand Down Expand Up @@ -155,28 +155,38 @@ Create a single source of truth:
from enum import Enum
from typing import List, Dict, Tuple


class DevelopmentalStage(str, Enum):
"""Unified C. elegans developmental stages."""
EARLY = "early" # Gastrulation, ~100+ cells, oval shape
BEAN = "bean" # Early morphogenesis, slight asymmetry
COMMA = "comma" # Clear C-shape, head/tail distinguishable
FOLD_1_5 = "1.5fold" # Elongation, ~1.5x eggshell
FOLD_2 = "2fold" # Further elongation, folding back
FOLD_3 = "3fold" # Tight coil, maximum compaction (pretzel)
HATCHING = "hatching" # Active emergence, shell breach visible
HATCHED = "hatched" # Fully emerged L1 larva

EARLY = "early" # Gastrulation, ~100+ cells, oval shape
BEAN = "bean" # Early morphogenesis, slight asymmetry
COMMA = "comma" # Clear C-shape, head/tail distinguishable
FOLD_1_5 = "1.5fold" # Elongation, ~1.5x eggshell
FOLD_2 = "2fold" # Further elongation, folding back
FOLD_3 = "3fold" # Tight coil, maximum compaction (pretzel)
HATCHING = "hatching" # Active emergence, shell breach visible
HATCHED = "hatched" # Fully emerged L1 larva

@classmethod
def ordered_list(cls) -> List['DevelopmentalStage']:
return [cls.EARLY, cls.BEAN, cls.COMMA, cls.FOLD_1_5,
cls.FOLD_2, cls.FOLD_3, cls.HATCHING, cls.HATCHED]
def ordered_list(cls) -> List["DevelopmentalStage"]:
return [
cls.EARLY,
cls.BEAN,
cls.COMMA,
cls.FOLD_1_5,
cls.FOLD_2,
cls.FOLD_3,
cls.HATCHING,
cls.HATCHED,
]

@classmethod
def get_order(cls, stage: 'DevelopmentalStage') -> int:
def get_order(cls, stage: "DevelopmentalStage") -> int:
return cls.ordered_list().index(stage)

@classmethod
def is_terminal(cls, stage: 'DevelopmentalStage') -> bool:
def is_terminal(cls, stage: "DevelopmentalStage") -> bool:
return stage == cls.HATCHED


Expand Down Expand Up @@ -339,15 +349,18 @@ from dataclasses import dataclass
from typing import List, Optional, Tuple
from .stages import DevelopmentalStage


@dataclass
class TransitionState:
"""Represents the current transition state."""

current_stage: DevelopmentalStage
confidence: float
is_transitioning: bool
next_stage: Optional[DevelopmentalStage]
transition_progress: float # 0.0 to 1.0


class TransitionDetector:
"""
Detects and tracks stage transitions with temporal smoothing.
Expand Down Expand Up @@ -481,12 +494,14 @@ from dataclasses import dataclass
from typing import Dict
from .stages import DevelopmentalStage


@dataclass
class CalibratedConfidence:
"""Calibrated confidence with interpretable thresholds."""
raw_score: float # 0.0 to 1.0 from VLM
calibrated_score: float # Adjusted based on stage difficulty
interpretation: str # "high", "medium", "low"

raw_score: float # 0.0 to 1.0 from VLM
calibrated_score: float # Adjusted based on stage difficulty
interpretation: str # "high", "medium", "low"

@property
def is_reliable(self) -> bool:
Expand All @@ -496,14 +511,14 @@ class CalibratedConfidence:
# Stage-specific confidence calibration
# Some stages are harder to classify, adjust thresholds accordingly
STAGE_DIFFICULTY = {
DevelopmentalStage.EARLY: 0.0, # Easy - distinct morphology
DevelopmentalStage.BEAN: 0.3, # Hard - brief, subtle
DevelopmentalStage.COMMA: 0.2, # Medium - can overlap with bean
DevelopmentalStage.FOLD_1_5: 0.2, # Medium
DevelopmentalStage.FOLD_2: 0.2, # Medium
DevelopmentalStage.FOLD_3: 0.1, # Easy - distinct pretzel
DevelopmentalStage.EARLY: 0.0, # Easy - distinct morphology
DevelopmentalStage.BEAN: 0.3, # Hard - brief, subtle
DevelopmentalStage.COMMA: 0.2, # Medium - can overlap with bean
DevelopmentalStage.FOLD_1_5: 0.2, # Medium
DevelopmentalStage.FOLD_2: 0.2, # Medium
DevelopmentalStage.FOLD_3: 0.1, # Easy - distinct pretzel
DevelopmentalStage.HATCHING: 0.25, # Medium-hard - brief window
DevelopmentalStage.HATCHED: 0.0, # Easy - distinct
DevelopmentalStage.HATCHED: 0.0, # Easy - distinct
}


Expand Down Expand Up @@ -587,7 +602,7 @@ Return JSON:
class HatchingDetector:
"""Specialized detector for hatching stages."""

def __init__(self, engine: 'PerceptionEngine'):
def __init__(self, engine: "PerceptionEngine"):
self.engine = engine
self.consecutive_hatching = 0
self.consecutive_hatched = 0
Expand All @@ -604,16 +619,16 @@ class HatchingDetector:
)

# Track consecutive detections for confirmation
if result.get('classification') == 'hatching':
if result.get("classification") == "hatching":
self.consecutive_hatching += 1
self.consecutive_hatched = 0
elif result.get('classification') == 'hatched':
elif result.get("classification") == "hatched":
self.consecutive_hatched += 1
# Require 2+ consecutive "hatched" to confirm
# (hatching can look like hatched momentarily)
if self.consecutive_hatched < 2:
result['classification'] = 'hatching'
result['note'] = 'Awaiting confirmation of hatched status'
result["classification"] = "hatching"
result["note"] = "Awaiting confirmation of hatched status"
else:
self.consecutive_hatching = 0
self.consecutive_hatched = 0
Expand Down
Loading
Loading