Spaces:
Running
Running
Download src/trace2env/workspace.py from Quanyu001/trace2env_demo: direct link, hf CLI and curl.
- Browser
- Download file 36.9 kB
-
https://huggingface.co/spaces/Quanyu001/trace2env_demo/resolve/main/src/trace2env/workspace.py
- Command line
-
hf download hf://spaces/Quanyu001/trace2env_demo/src/trace2env/workspace.py
-
curl -L -o workspace.py https://huggingface.co/spaces/Quanyu001/trace2env_demo/resolve/main/src/trace2env/workspace.py
36.9 kB
| """Workspace tools: how the world-model agent inspects knowledge, state, and episodic memory. | |
| Every tool returns a bounded, JSON-serializable view and registers the artifact identifiers it | |
| exposed, so a submitted transition can only cite what the agent actually retrieved. Rules are | |
| offered as dry-run tools rather than executed behind the agent's back; nothing here mutates the | |
| session, and the harness alone verifies and commits a transition. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Any, Callable | |
| from trace2env.engine import ( | |
| MISSING, | |
| apply_mutations, | |
| check_invariants, | |
| deterministic_plan, | |
| evaluate_condition, | |
| get_path, | |
| render_template, | |
| ) | |
| from trace2env.eligibility import rule_eligible | |
| from trace2env.memory import EpisodicMemory | |
| from trace2env.models import ( | |
| SUBMIT_TOOL_NAME, | |
| EnvironmentState, | |
| MemoryEntry, | |
| NormalizedAction, | |
| StateMutation, | |
| TransitionRule, | |
| TransitionSubmission, | |
| ) | |
| from trace2env.package import KNOWLEDGE_KINDS, EnvironmentPackage, PackageInspector | |
| from trace2env.trace_corpus import TraceCorpus, action_query | |
| from trace2env.validation import validate_mutations, validate_state_types | |
| _ABSENT = object() | |
| ABSENCE_MEANS_UNKNOWN = ("An absent key or path means 'not observed in this episode yet', not 'does not exist': " | |
| "the tracker records only what observations established.") | |
| def clip_text(text: str, limit: int) -> str: | |
| if limit <= 0 or len(text) <= limit: | |
| return text | |
| return text[:limit] + f"\n... [truncated {len(text) - limit} characters]" | |
| def clip_span(text: str, limit: int, *, turn: int | None = None) -> str: | |
| """Head-and-tail clip that tells the agent how to read the omitted middle exactly (``read_turn``).""" | |
| if limit <= 0 or len(text) <= limit: | |
| return text | |
| head = text[: limit * 3 // 4] | |
| tail = text[-(limit - len(head)):] if limit > len(head) else "" | |
| hint = f"read_turn(turn={turn}, offset={len(head)})" if turn is not None else "read_turn" | |
| return f"{head}\n[... {len(text) - len(head) - len(tail)} characters omitted; {hint} shows them ...]\n{tail}" | |
| def compact_action(action: NormalizedAction | dict[str, Any]) -> dict[str, Any]: | |
| """An action view without redundant copies: ``keystrokes`` repeat ``commands``/``command``; ``raw`` repeats both.""" | |
| data = action.model_dump(mode="json", exclude={"raw"}) if isinstance(action, NormalizedAction) else dict(action) | |
| data.pop("raw", None) | |
| arguments = dict(data.get("arguments") or {}) | |
| if "keystrokes" in arguments and ("commands" in arguments or "command" in arguments): | |
| entries = arguments.pop("keystrokes") | |
| if isinstance(entries, list): | |
| total = sum(float(entry.get("duration", 0) or 0) for entry in entries if isinstance(entry, dict)) | |
| arguments["duration_total"] = round(total, 3) | |
| data["arguments"] = arguments | |
| return data | |
| def clip_value(value: Any, limit: int) -> Any: | |
| if isinstance(value, str): | |
| return clip_text(value, limit) | |
| if isinstance(value, dict): | |
| return {str(key): clip_value(item, limit) for key, item in value.items()} | |
| if isinstance(value, list): | |
| return [clip_value(item, limit) for item in value] | |
| return value | |
| def flatten(value: Any, prefix: str = "") -> dict[str, Any]: | |
| """Dotted leaf paths; empty containers contribute no leaves, so filling a namespace is not itself a change.""" | |
| if isinstance(value, dict): | |
| result: dict[str, Any] = {} | |
| for key, item in value.items(): | |
| result.update(flatten(item, f"{prefix}.{key}" if prefix else str(key))) | |
| return result | |
| return {prefix: value} if prefix else {} | |
| def state_diff(before: EnvironmentState, after: EnvironmentState, *, limit: int = 50, text_limit: int = 300) -> dict[str, Any]: | |
| old = flatten(before.model_dump(mode="json")) | |
| new = flatten(after.model_dump(mode="json")) | |
| changes: dict[str, Any] = {} | |
| for path in sorted(set(old) | set(new)): | |
| if path.startswith("revision") or old.get(path, _ABSENT) == new.get(path, _ABSENT): | |
| continue | |
| changes[path] = { | |
| "before": clip_value(old[path], text_limit) if path in old else "<absent>", | |
| "after": clip_value(new[path], text_limit) if path in new else "<absent>", | |
| } | |
| if len(changes) >= limit: | |
| changes["..."] = "further changes omitted" | |
| break | |
| return changes | |
| def state_summary(state: EnvironmentState, *, text_limit: int = 160) -> dict[str, Any]: | |
| summary: dict[str, Any] = {"step": state.step, "revision": state.revision, "pending_events": len(state.pending_events)} | |
| for namespace in ("world", "session", "surface", "epistemic"): | |
| summary[namespace] = clip_value(getattr(state, namespace), text_limit) | |
| return summary | |
| class Tool: | |
| name: str | |
| description: str | |
| parameters: dict[str, Any] | |
| handler: Callable[[dict[str, Any]], Any] | None = None | |
| def spec(self) -> dict[str, Any]: | |
| return {"type": "function", "function": {"name": self.name, "description": self.description, "parameters": self.parameters}} | |
| class WorkspaceTools: | |
| package: EnvironmentPackage | |
| inspector: PackageInspector | |
| memory: EpisodicMemory | None | |
| state: EnvironmentState | |
| action: NormalizedAction | |
| text_limit: int = 1500 | |
| list_limit: int = 8 | |
| minimum_confidence: float = 0.0 | |
| # Online-harness features (see RuntimeHarness): "history" adds read_turn and full-text recall | |
| # hits, "compact" slims duplicated views, "unknown" spells out absence semantics. | |
| features: set[str] = field(default_factory=set) | |
| brief_action_type: str | None = None # the action whose retrieval the brief already shows | |
| turn_chars: int = 6000 # verbatim chars per memory observation before head/tail clipping | |
| recall_full: int = 3 # memory hits returned in full (up to full_chars) per recall/recent_turns call | |
| full_chars: int = 24000 | |
| # Raw construction traces offered instead of reconstructed knowledge (the agentic_raw_traces control). | |
| trace_corpus: TraceCorpus | None = None | |
| # False for the trace2env_no_state control: no read_state / state_schema / apply_rule / dry_run. | |
| state_tools: bool = True | |
| # False for the harness_only control: no list_actions / inspect_action / search_knowledge / read_evidence. | |
| knowledge_tools: bool = True | |
| # Harness v5.2: applicability decisions and sanitized views for every package item shown (None = v5.1 behaviour). | |
| gate: Any = None | |
| withhold_templates: bool = False # option B: rule templates withheld when the official input documents the format | |
| retrieved_ids: set[str] = field(default_factory=set) | |
| discovered_rules: dict[str, TransitionRule] = field(default_factory=dict) | |
| calls: list[dict[str, Any]] = field(default_factory=list) | |
| # βββ Catalog ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def tools(self) -> list[Tool]: | |
| string = {"type": "string"} | |
| integer = {"type": "integer", "minimum": 1} | |
| return [ | |
| *([Tool("list_actions", "List the environment's known actions with their aliases and argument names.", | |
| {"type": "object", "properties": {}}, self._list_actions), | |
| Tool("inspect_action", "Retrieve an action's contract, the rules eligible for it now, its observation " | |
| "contracts, demonstrations, notes, and the global invariants.", | |
| {"type": "object", "properties": {"action_type": string}, "required": ["action_type"]}, self._inspect_action), | |
| Tool("search_knowledge", "Full-text search over reconstructed knowledge: rules, notes, demonstrations, " | |
| "evidence (real observed transitions), and actions. Use it for formats, error shapes, and examples.", | |
| {"type": "object", "properties": {"query": string, "kinds": {"type": "array", "items": {"type": "string", "enum": KNOWLEDGE_KINDS}}, | |
| "limit": integer}, "required": ["query"]}, self._search_knowledge)] if self.knowledge_tools else []), | |
| *([Tool("read_state", "Read the current session state: a dotted path such as world.balance, or omit the " | |
| "path for a summary of every namespace.", | |
| {"type": "object", "properties": {"path": string}}, self._read_state), | |
| Tool("state_schema", "Describe declared state fields (types, mutability, descriptions), optionally under a path prefix.", | |
| {"type": "object", "properties": {"prefix": string}}, self._state_schema)] if self.state_tools else []), | |
| Tool("recall", "Search this episode's memory of earlier actions and real observations.", | |
| {"type": "object", "properties": {"query": string, "limit": integer}, "required": ["query"]}, self._recall), | |
| Tool("recent_turns", "Return the most recent turns of this episode with their observations.", | |
| {"type": "object", "properties": {"limit": integer}}, self._recent_turns), | |
| *([Tool("read_turn", "Read an exact span of one earlier turn's real observation (turn numbers as shown in " | |
| "recent_memory/recall; turn 0 is the initial screen). Page with offset/length to reproduce long " | |
| "content verbatim.", | |
| {"type": "object", "properties": {"turn": {"type": "integer", "minimum": 0}, "offset": {"type": "integer", "minimum": 0}, | |
| "length": integer}, "required": ["turn"]}, self._read_turn)] if "history" in self.features else []), | |
| *([Tool("read_evidence", "Read one raw turn of the episodes this package was built from: its action and an exact " | |
| "span of its real observation (page with offset/length), plus the neighbouring turns' action types. " | |
| "Ids come from similar_turns, search_knowledge (evidence/demonstration), or demonstrations.", | |
| {"type": "object", "properties": {"id": string, "offset": {"type": "integer", "minimum": 0}, "length": integer}, | |
| "required": ["id"]}, self._read_evidence)] if "evidence" in self.features and self.knowledge_tools else []), | |
| *([Tool("search_traces", "Full-text search over the raw action -> observation turns of other recorded episodes " | |
| "of this environment (different tasks). Returns turn ids, the action, and a snippet; use " | |
| "read_trace_turn for the exact observation.", | |
| {"type": "object", "properties": {"query": string, "limit": integer}, "required": ["query"]}, self._search_traces), | |
| Tool("read_trace_turn", "Read one raw trace turn: its action and an exact span of its real observation " | |
| "(page with offset/length), plus the neighbouring turns' action types.", | |
| {"type": "object", "properties": {"episode": string, "turn": {"type": "integer", "minimum": 0}, | |
| "offset": {"type": "integer", "minimum": 0}, "length": integer}, "required": ["episode", "turn"]}, | |
| self._read_trace_turn)] if self.trace_corpus is not None else []), | |
| *([Tool("apply_rule", "Dry-run a rule against the current state: whether it applies, its resolved effects, " | |
| "the resulting state diff, the rendered observation when a template exists, and invariant checks.", | |
| {"type": "object", "properties": {"rule_id": string}, "required": ["rule_id"]}, self._apply_rule), | |
| Tool("dry_run", "Apply candidate effects to a copy of the state and report the diff and invariant checks.", | |
| {"type": "object", "properties": {"effects": {"type": "array", "items": StateMutation.model_json_schema()}}, | |
| "required": ["effects"]}, self._dry_run)] if self.state_tools else []), | |
| Tool(SUBMIT_TOOL_NAME, "Finish: submit the transition (effects, outcome, exact observation, rule_ids, " | |
| "citations, uncertainty). This ends the simulation of this action.", | |
| TransitionSubmission.model_json_schema(), None), | |
| ] | |
| def specs(self) -> list[dict[str, Any]]: | |
| return [tool.spec() for tool in self.tools()] | |
| def call(self, name: str, arguments: dict[str, Any]) -> Any: | |
| tool = next((item for item in self.tools() if item.name == name), None) | |
| record: dict[str, Any] = {"tool": name, "arguments": clip_value(arguments, 200)} | |
| if tool is None or tool.handler is None: | |
| record["error"] = f"Unknown tool {name!r}" | |
| self.calls.append(record) | |
| return {"error": record["error"], "available_tools": [item.name for item in self.tools()]} | |
| try: | |
| result = tool.handler(arguments or {}) | |
| except Exception as exc: # noqa: BLE001 - tool failures are reported to the agent, not raised | |
| result = {"error": f"{type(exc).__name__}: {exc}"} | |
| if isinstance(result, dict) and "error" in result: | |
| record["error"] = result["error"] | |
| self.calls.append(record) | |
| return result | |
| # βββ Knowledge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _list_actions(self, arguments: dict[str, Any]) -> Any: | |
| self.retrieved_ids.add("schema:actions") | |
| return [ | |
| {"name": spec.name, "description": spec.description, "aliases": spec.aliases, "arguments": sorted(spec.arguments)} | |
| for spec in self.package.action_schema.actions | |
| ] | |
| # βββ Harness v5.2 gate helpers (no-ops when self.gate is None) ββββββββββββ | |
| def _gate_record(self, reference: str): | |
| evidence_id = self.inspector.resolve_evidence_id(reference) | |
| return self.inspector._evidence_index()["by_id"].get(evidence_id) if evidence_id else None | |
| def _gate_summary(self, summary: dict[str, Any], *, kind: str = "evidence") -> dict[str, Any]: | |
| """An evidence summary (hit or search result) with its applicability and a sanitized snippet when needed.""" | |
| if self.gate is None: | |
| return summary | |
| record = self._gate_record(summary.get("id", "")) | |
| if record is None: | |
| return summary | |
| decision = self.gate.decide_evidence(record, kind=kind) | |
| if decision.disposition == "rejected": | |
| return None # withheld (format_only ablation): the item is recorded, not shown | |
| view = dict(summary) | |
| if decision.disposition == "sanitized": | |
| for key in ("snippet", "action_arguments", "observation_head"): | |
| if key in view: | |
| view[key] = self.gate.sanitize_value(view[key], record, decision) | |
| view["applicability"] = decision.view() | |
| return view | |
| def _gate_notes(self, notes: list[Any]) -> list[Any]: | |
| """Notes with their applicability; a note contradicted by this episode's transcript is dropped.""" | |
| if self.gate is None: | |
| return notes | |
| kept = [] | |
| for note in notes: | |
| data = note if isinstance(note, dict) else note.model_dump(mode="json") | |
| decision = self.gate.decide_note(data) | |
| if decision.disposition == "rejected": | |
| continue | |
| kept.append({**data, "applicability": decision.view()}) | |
| return kept | |
| def _inspect_action(self, arguments: dict[str, Any]) -> Any: | |
| result = self.inspector.inspect_action(str(arguments.get("action_type", "")), state=self.state) | |
| if self.gate is not None: | |
| result = dict(result) | |
| result["notes"] = self._gate_notes(result.get("notes") or []) | |
| if getattr(self.gate, "judge_llm", None) is not None: | |
| self.gate.prejudge([r for r in (self._gate_record(d.get("id", "")) for d in result.get("demonstrations") or []) if r is not None]) | |
| demos = [] | |
| for demo in result.get("demonstrations") or []: | |
| record = self._gate_record(demo.get("id", "")) | |
| if record is None: | |
| demos.append(demo) | |
| continue | |
| decision = self.gate.decide_evidence(record, kind="demonstration") | |
| if decision.disposition == "rejected": | |
| continue | |
| view = dict(demo) | |
| if decision.disposition == "sanitized": | |
| view["observation"] = self.gate.sanitize(str(demo.get("observation") or ""), record, decision) | |
| view["action"] = self.gate.sanitize_value(demo.get("action"), record, decision) | |
| view["applicability"] = decision.view() | |
| demos.append(view) | |
| result["demonstrations"] = demos | |
| for item in result["rules"]: | |
| rule = TransitionRule.model_validate(item) | |
| self.discovered_rules.setdefault(rule.id, rule) | |
| if self.withhold_templates: # option B: the returned view carries no template (the rule objects above are intact) | |
| result = dict(result) | |
| result["rules"] = [{**item, "observation_template": None, | |
| "template_withheld": "the official input documents this action's observation format; follow its example"} | |
| for item in result["rules"]] | |
| self.retrieved_ids.update(result["artifact_ids"]) | |
| self.retrieved_ids.update(f"invariant:{item['id']}" for item in result["invariants"]) | |
| if "compact" in self.features and result["canonical_action_type"] == self.brief_action_type: | |
| # The brief already carries this action's contract, notes, contracts, and demonstrations; | |
| # only the rule bodies (conditions, effects) add information. | |
| return { | |
| "note": "This action's contract, notes, observation contracts, and demonstrations are already in your " | |
| "brief (retrieved); only the rule bodies are listed here. Use apply_rule to dry-run one.", | |
| "canonical_action_type": result["canonical_action_type"], | |
| "rules": [{key: item[key] for key in ("id", "description", "conditions", "effects", "outcome", "confidence", "status")} | |
| for item in result["rules"]], | |
| "invariant_ids": [item["id"] for item in result["invariants"]], | |
| } | |
| return clip_value(result, self.text_limit) | |
| def _search_knowledge(self, arguments: dict[str, Any]) -> Any: | |
| limit = min(int(arguments.get("limit") or self.list_limit), 25) | |
| kinds = arguments.get("kinds") or None | |
| results = self.inspector.search_knowledge(str(arguments.get("query", "")), kinds=kinds, limit=limit) | |
| if self.gate is not None and getattr(self.gate, "judge_llm", None) is not None: | |
| self.gate.prejudge([r for r in (self._gate_record(item["ref_id"]) for item in results if item["kind"] in ("evidence", "demonstration")) if r is not None]) | |
| shaped: list[Any] = [] | |
| for item in results: | |
| self.retrieved_ids.add(item["id"]) | |
| if item["kind"] == "rule": | |
| rule = next((rule for rule in self.package.rules if rule.id == item["ref_id"]), None) | |
| if rule is not None: | |
| self.discovered_rules.setdefault(rule.id, rule) | |
| if "evidence" in self.features and item["kind"] in ("evidence", "demonstration"): | |
| # A raw turn is never clipped into the result; the hit says how long it is and how to read it exactly. | |
| evidence_id = self.inspector.resolve_evidence_id(item["ref_id"]) | |
| summary = self.inspector.evidence_summary(evidence_id, snippet=item.get("snippet")) if evidence_id else None | |
| if summary is not None: | |
| self.retrieved_ids.add(summary["id"]) | |
| gated = self._gate_summary(summary, kind=item["kind"]) | |
| if gated is not None: | |
| shaped.append({**gated, "kind": item["kind"]}) | |
| continue | |
| if self.gate is not None: | |
| if item["kind"] == "note": | |
| kept = self._gate_notes([{"id": item["ref_id"], "statement": item.get("text") or ""}]) | |
| if not kept: | |
| continue # contradicted by this episode's transcript | |
| item = {**item, "applicability": kept[0]["applicability"]} | |
| elif item["kind"] in ("evidence", "demonstration"): | |
| record = self._gate_record(item["ref_id"]) | |
| if record is not None: | |
| decision = self.gate.decide_evidence(record, kind=item["kind"]) | |
| if decision.disposition == "rejected": | |
| continue | |
| item = dict(item) | |
| if decision.disposition == "sanitized": | |
| for key in ("text", "snippet"): | |
| if key in item: | |
| item[key] = self.gate.sanitize(str(item[key]), record, decision) | |
| item["applicability"] = decision.view() | |
| shaped.append(clip_value(item, self.text_limit)) | |
| return shaped | |
| # βββ Evidence tier (v3 read path) βββββββββββββββββββββββββββββββββββββββββ | |
| def evidence_hits(self, query: str, limit: int | None = None) -> list[dict[str, Any]]: | |
| """Raw turns whose action or observation matches, as summaries; registered as citable.""" | |
| if not query.strip(): | |
| return [] | |
| hits = self.inspector.search_evidence(query, min(int(limit or self.list_limit), 25)) | |
| self.retrieved_ids.update(hit["id"] for hit in hits) | |
| if self.gate is not None and getattr(self.gate, "judge_llm", None) is not None: | |
| self.gate.prejudge([r for r in (self._gate_record(hit["id"]) for hit in hits) if r is not None]) # one batched call | |
| return [gated for gated in (self._gate_summary(hit) for hit in hits) if gated is not None] | |
| def demo_view(self, demo: Any) -> dict[str, Any]: | |
| """A demonstration as a summary with a read pointer instead of a clipped observation.""" | |
| data = demo if isinstance(demo, dict) else demo.model_dump(mode="json") | |
| evidence_id = self.inspector.resolve_evidence_id(data["id"]) | |
| observation = data.get("observation") or "" | |
| view = { | |
| "id": f"demo:{data['id']}", "action": compact_action(data["action"]), "outcome": data.get("outcome"), | |
| "observation_chars": len(observation), "observation_head": observation[:400], | |
| } | |
| if evidence_id is not None: | |
| view["evidence"] = f"evidence:{evidence_id}" | |
| view["read"] = f"read_evidence(id='evidence:{evidence_id}')" | |
| self.retrieved_ids.add(f"evidence:{evidence_id}") | |
| if self.gate is not None: | |
| record = self._gate_record(f"evidence:{evidence_id}") | |
| if record is not None: | |
| decision = self.gate.decide_evidence(record, kind="demonstration") | |
| if decision.disposition == "rejected": | |
| return {"id": view["id"], "withheld": True, "applicability": decision.view()} | |
| if decision.disposition == "sanitized": | |
| view["observation_head"] = self.gate.sanitize(view["observation_head"], record, decision) | |
| view["action"] = self.gate.sanitize_value(view["action"], record, decision) | |
| view["applicability"] = decision.view() | |
| return view | |
| def _read_evidence(self, arguments: dict[str, Any]) -> Any: | |
| evidence_id = self.inspector.resolve_evidence_id(str(arguments.get("id", ""))) | |
| if evidence_id is None: | |
| return {"error": f"Unknown evidence id {arguments.get('id')!r}; use similar_turns, search_knowledge, or a demonstration's id"} | |
| offset = max(0, int(arguments.get("offset") or 0)) | |
| length = max(1, min(int(arguments.get("length") or self.turn_chars), self.full_chars)) | |
| view = self.inspector.evidence_view(evidence_id, offset=offset, length=length) | |
| if view is not None: | |
| self.retrieved_ids.add(view["id"]) | |
| if self.gate is not None: | |
| record = self._gate_record(view["id"]) | |
| if record is not None: | |
| decision = self.gate.decide_evidence(record) | |
| if decision.disposition == "rejected": | |
| return {"id": view["id"], "withheld": "This turn is task-specific or uncertain evidence and is not available " | |
| "in this configuration; predict from the episode's transcript and state.", "applicability": decision.view()} | |
| view = dict(view) | |
| if decision.disposition == "sanitized": | |
| view["text"] = self.gate.sanitize(view["text"], record, decision) | |
| view["action_arguments"] = self.gate.sanitize_value(view.get("action_arguments"), record, decision) | |
| view["applicability"] = decision.view() | |
| return view | |
| # βββ Raw traces (agentic_raw_traces control) ββββββββββββββββββββββββββββββ | |
| def trace_hits(self, query: str, limit: int | None = None) -> list[dict[str, Any]]: | |
| """Search the raw-trace corpus and register the hits as citable artifacts.""" | |
| if self.trace_corpus is None: | |
| return [] | |
| hits = self.trace_corpus.search(query, min(int(limit or self.list_limit), 25)) | |
| self.retrieved_ids.update(hit["id"] for hit in hits) | |
| return clip_value(hits, self.text_limit) | |
| def _search_traces(self, arguments: dict[str, Any]) -> Any: | |
| return self.trace_hits(str(arguments.get("query", "")), arguments.get("limit")) | |
| def _read_trace_turn(self, arguments: dict[str, Any]) -> Any: | |
| if self.trace_corpus is None: | |
| return {"error": "No raw-trace corpus is attached to this workspace"} | |
| episode = str(arguments.get("episode", "")) | |
| turn = int(arguments.get("turn", -1)) | |
| offset = max(0, int(arguments.get("offset") or 0)) | |
| length = max(1, min(int(arguments.get("length") or self.turn_chars), self.full_chars)) | |
| result = self.trace_corpus.read(episode, turn, offset=offset, length=length) | |
| if result is None: | |
| return {"error": f"No trace turn {episode}:{turn}; use search_traces to find turn ids"} | |
| self.retrieved_ids.add(result["id"]) | |
| return result | |
| def _state_schema(self, arguments: dict[str, Any]) -> Any: | |
| prefix = arguments.get("prefix") or None | |
| fields = self.inspector.inspect_state(prefix) | |
| self.retrieved_ids.update(f"schema:{item['path']}" for item in fields) | |
| return fields[: self.list_limit * 4] | |
| # βββ State and memory βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _read_state(self, arguments: dict[str, Any]) -> Any: | |
| path = arguments.get("path") | |
| if not path: | |
| self.retrieved_ids.add("state:*") | |
| summary = state_summary(self.state, text_limit=self.text_limit // 4) | |
| if "unknown" in self.features: | |
| summary["semantics"] = ABSENCE_MEANS_UNKNOWN | |
| return summary | |
| value = get_path(self.state.model_dump(mode="json"), str(path), MISSING) | |
| self.retrieved_ids.add(f"state:{path}") | |
| if value is MISSING: | |
| missing: dict[str, Any] = {"path": path, "missing": True} | |
| if "unknown" in self.features: | |
| missing["meaning"] = ABSENCE_MEANS_UNKNOWN | |
| return missing | |
| if "unknown" in self.features and isinstance(value, str): | |
| # Exact values can be paged like turns so long contents are never reproduced from a clip. | |
| offset = max(0, int(arguments.get("offset") or 0)) | |
| length = max(1, min(int(arguments.get("length") or self.full_chars), self.full_chars)) | |
| return {"path": path, "total_chars": len(value), "offset": offset, "text": value[offset:offset + length], | |
| "next_offset": offset + length if offset + length < len(value) else None} | |
| return {"path": path, "value": clip_value(value, self.text_limit)} | |
| def _memory_view(self, entry: MemoryEntry, *, full: bool = False) -> dict[str, Any]: | |
| identifier = f"memory:{entry.id}" | |
| self.retrieved_ids.add(identifier) | |
| if "history" in self.features: | |
| observation = entry.observation[: self.full_chars] if full else clip_span(entry.observation, self.turn_chars, turn=entry.turn) | |
| if full and len(entry.observation) > self.full_chars: | |
| observation += f"\n[... {len(entry.observation) - self.full_chars} more characters; read_turn(turn={entry.turn}, offset={self.full_chars}) ...]" | |
| else: | |
| observation = clip_text(entry.observation, self.text_limit) | |
| action = compact_action(entry.action) if "compact" in self.features else entry.action.model_dump(mode="json", exclude={"raw"}) | |
| return { | |
| "id": identifier, | |
| "turn": entry.turn, | |
| "kind": entry.kind, | |
| "action": action, | |
| "observation": observation, | |
| "observation_chars": len(entry.observation), | |
| "effects": [effect.model_dump(mode="json", exclude_none=True) for effect in entry.effects], | |
| } | |
| def _views(self, entries: list[MemoryEntry], *, newest_full: bool) -> list[dict[str, Any]]: | |
| """Memory views where the most relevant ``recall_full`` entries are complete and the rest are clipped.""" | |
| full_ids = {entry.id for entry in (entries[-self.recall_full:] if newest_full else entries[: self.recall_full])} | |
| return [self._memory_view(entry, full=entry.id in full_ids) for entry in entries] | |
| def _recall(self, arguments: dict[str, Any]) -> Any: | |
| if self.memory is None: | |
| return [] | |
| limit = min(int(arguments.get("limit") or self.list_limit), 25) | |
| return self._views(self.memory.search(str(arguments.get("query", "")), limit), newest_full=False) | |
| def _recent_turns(self, arguments: dict[str, Any]) -> Any: | |
| if self.memory is None: | |
| return [] | |
| limit = min(int(arguments.get("limit") or self.list_limit), 25) | |
| return self._views(self.memory.recent(limit), newest_full=True) | |
| def _read_turn(self, arguments: dict[str, Any]) -> Any: | |
| if self.memory is None: | |
| return {"error": "No episodic memory in this session"} | |
| turn = int(arguments.get("turn", -1)) | |
| entries = self.memory.by_turn(turn) | |
| if not entries: | |
| return {"error": f"No recorded turn {turn}", "recorded_turns": sorted({e.turn for e in self.memory.recent(200)})} | |
| entry = entries[0] | |
| offset = max(0, int(arguments.get("offset") or 0)) | |
| length = max(1, min(int(arguments.get("length") or self.turn_chars), self.full_chars)) | |
| text = entry.observation | |
| self.retrieved_ids.add(f"memory:{entry.id}") | |
| return { | |
| "id": f"memory:{entry.id}", "turn": entry.turn, "kind": entry.kind, | |
| "action": compact_action(entry.action), | |
| "total_chars": len(text), "offset": offset, "length": min(length, max(0, len(text) - offset)), | |
| "text": text[offset:offset + length], | |
| "next_offset": offset + length if offset + length < len(text) else None, | |
| } | |
| # βββ Simulation dry-runs ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _spec(self): | |
| return next((spec for spec in self.package.action_schema.actions if spec.name == self.action.type), None) | |
| def _render_context(self, after: EnvironmentState, outcome: str) -> dict[str, Any]: | |
| return { | |
| "action": self.action.model_dump(mode="python"), | |
| "state_before": self.state.model_dump(mode="python"), | |
| "state_after": after.model_dump(mode="python"), | |
| "outcome": outcome, | |
| } | |
| def _apply_rule(self, arguments: dict[str, Any]) -> Any: | |
| rule_id = str(arguments.get("rule_id", "")) | |
| rule = next((item for item in self.package.rules if item.id == rule_id), None) or self.discovered_rules.get(rule_id) | |
| if rule is None: | |
| return {"error": f"Unknown rule {rule_id!r}; use inspect_action or search_knowledge to find rule ids"} | |
| self.retrieved_ids.add(f"rule:{rule.id}") | |
| self.discovered_rules.setdefault(rule.id, rule) | |
| if rule.action_type != self.action.type: | |
| return {"applicable": False, "rule_id": rule.id, "reason": f"Rule is for action {rule.action_type!r}, not {self.action.type!r}"} | |
| if not rule_eligible(rule, self.package.scope, self.state, minimum_confidence=self.minimum_confidence): | |
| return {"applicable": False, "rule_id": rule.id, "reason": "Rule is not eligible here (status, confidence, or scope)"} | |
| failed = [condition.model_dump(mode="json", exclude_none=True) for condition in rule.conditions | |
| if not evaluate_condition(condition, self.state, self.action)] | |
| if failed: | |
| return {"applicable": False, "rule_id": rule.id, "reason": "Conditions do not hold on the current state", "failed_conditions": failed} | |
| try: | |
| plan = deterministic_plan(rule, self.action) | |
| validate_mutations(plan.effects, self.package.state_schema, self._spec()) | |
| candidate = apply_mutations(self.state, plan.effects, action=self.action) | |
| validate_state_types(candidate, self.package.state_schema) | |
| except (KeyError, ValueError) as exc: | |
| return {"applicable": False, "rule_id": rule.id, "reason": f"Effects cannot be applied: {exc}"} | |
| contract = next((item for item in self.package.renderers if item.id == plan.renderer_id), None) | |
| template = plan.observation_template or (contract.template if contract else None) | |
| observation: str | None = None | |
| withheld = bool(template) and self.withhold_templates | |
| if template and not withheld: | |
| try: | |
| observation = render_template(template, self._render_context(candidate, plan.outcome.value)) | |
| except KeyError as exc: | |
| observation = None | |
| invariants = check_invariants(self.package.invariants, candidate, self.action) | |
| return { | |
| "applicable": True, | |
| "rule_id": rule.id, | |
| "description": rule.description, | |
| "outcome": plan.outcome.value, | |
| "effects": [effect.model_dump(mode="json", exclude_none=True) for effect in plan.effects], | |
| "state_diff": state_diff(self.state, candidate), | |
| "observation": clip_text(observation, self.text_limit) if observation is not None else None, | |
| **({"observation_withheld": "the official input documents this action's observation format; follow its example"} if withheld else {}), | |
| "invariants_ok": invariants.accepted, | |
| "invariant_issues": [issue.message for issue in invariants.issues], | |
| "how_to_submit": "To apply this rule, submit exactly these effects with rule_ids=[rule_id]; " | |
| "otherwise leave rule_ids empty and list the rule in citations.", | |
| } | |
| def _dry_run(self, arguments: dict[str, Any]) -> Any: | |
| try: | |
| effects = [StateMutation.model_validate(item) for item in arguments.get("effects", [])] | |
| validate_mutations(effects, self.package.state_schema, self._spec()) | |
| candidate = apply_mutations(self.state, effects, action=self.action) | |
| validate_state_types(candidate, self.package.state_schema) | |
| except (KeyError, ValueError) as exc: | |
| return {"ok": False, "error": f"{type(exc).__name__}: {exc}"} | |
| invariants = check_invariants(self.package.invariants, candidate, self.action) | |
| return { | |
| "ok": True, | |
| "state_diff": state_diff(self.state, candidate), | |
| "invariants_ok": invariants.accepted, | |
| "invariant_issues": [issue.message for issue in invariants.issues], | |
| } | |