tracecite 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. tracecite/__init__.py +137 -0
  2. tracecite/core/__init__.py +7 -0
  3. tracecite/extension/__init__.py +104 -0
  4. tracecite/integrations/__init__.py +3 -0
  5. tracecite/integrations/agent_profile.py +195 -0
  6. tracecite/integrations/agent_projection.py +173 -0
  7. tracecite/integrations/cli.py +1225 -0
  8. tracecite/integrations/evidence_ledger.py +293 -0
  9. tracecite/knowledge/__init__.py +1286 -0
  10. tracecite/output_layout.py +77 -0
  11. tracecite/runtime/__init__.py +157 -0
  12. tracecite/runtime/assertions.py +358 -0
  13. tracecite/runtime/cli.py +9 -0
  14. tracecite/runtime/investigation.py +2735 -0
  15. tracecite/runtime/investigation_compare.py +902 -0
  16. tracecite/runtime/investigation_summary.py +826 -0
  17. tracecite/runtime/reporting.py +168 -0
  18. tracecite/runtime/runtime.py +161 -0
  19. tracecite/runtime/scenario.py +1940 -0
  20. tracecite/runtime/schema.py +273 -0
  21. tracecite/runtime/schema_compat.py +602 -0
  22. tracecite/runtime/tools.py +1463 -0
  23. tracecite-0.1.0.dist-info/METADATA +159 -0
  24. tracecite-0.1.0.dist-info/RECORD +48 -0
  25. tracecite-0.1.0.dist-info/WHEEL +5 -0
  26. tracecite-0.1.0.dist-info/entry_points.txt +3 -0
  27. tracecite-0.1.0.dist-info/licenses/LICENSE +22 -0
  28. tracecite-0.1.0.dist-info/top_level.txt +2 -0
  29. tracecite_core/__init__.py +158 -0
  30. tracecite_core/cli.py +139 -0
  31. tracecite_core/events.py +257 -0
  32. tracecite_core/format_probe.py +629 -0
  33. tracecite_core/immutable.py +19 -0
  34. tracecite_core/live_cut.py +150 -0
  35. tracecite_core/log_filter.py +2 -0
  36. tracecite_core/matcher.py +745 -0
  37. tracecite_core/output_layout.py +84 -0
  38. tracecite_core/plugin_sdk.py +164 -0
  39. tracecite_core/preprocess.py +121 -0
  40. tracecite_core/records.py +63 -0
  41. tracecite_core/run.py +379 -0
  42. tracecite_core/sample.py +555 -0
  43. tracecite_core/segment_store.py +157 -0
  44. tracecite_core/segmenter.py +597 -0
  45. tracecite_core/source.py +557 -0
  46. tracecite_core/state_file.py +66 -0
  47. tracecite_core/survey.py +605 -0
  48. tracecite_core/text_filter.py +1441 -0
tracecite/__init__.py ADDED
@@ -0,0 +1,137 @@
1
+ """TraceCite: an extensible evidence runtime for AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from .runtime import (
8
+ AgentResult,
9
+ BUDGET_POLICY_SCHEMA_VERSION,
10
+ BudgetExhausted,
11
+ BudgetPolicy,
12
+ BudgetReservation,
13
+ EvidencePointer,
14
+ FINDING_OUTCOMES,
15
+ HYPOTHESIS_STATUSES,
16
+ INVESTIGATION_SCHEMA_VERSION,
17
+ INVESTIGATION_STATUSES,
18
+ STOP_KINDS,
19
+ InvestigationError,
20
+ InvestigationCacheStore,
21
+ InvestigationCompareError,
22
+ InvestigationCompareLimits,
23
+ InvestigationState,
24
+ InvestigationStore,
25
+ InvestigationSummaryError,
26
+ RESULT_OUTCOMES,
27
+ RESULT_SCHEMA_VERSION,
28
+ SCENARIO_SCHEMA_VERSION,
29
+ ScenarioDocument,
30
+ ScenarioError,
31
+ ScenarioProfile,
32
+ ScenarioRuntime,
33
+ SUMMARY_SCHEMA_VERSION,
34
+ COMPARE_SCHEMA_VERSION,
35
+ TIMELINE_SCHEMA_VERSION,
36
+ SummaryLimits,
37
+ expand,
38
+ attach_investigation_result,
39
+ create_investigation,
40
+ load_investigation,
41
+ investigation_summary,
42
+ propose_candidate,
43
+ propose_knowledge_candidate,
44
+ peek,
45
+ probe,
46
+ run,
47
+ sample,
48
+ search,
49
+ survey,
50
+ summarize_investigation,
51
+ compare_investigation,
52
+ compare_investigations,
53
+ investigation_timeline,
54
+ timeline_investigation,
55
+ verify,
56
+ )
57
+ from .knowledge import (
58
+ GovernancePolicy,
59
+ KnowledgeCandidate,
60
+ KnowledgeGovernanceError,
61
+ KnowledgeGovernanceStore,
62
+ KnowledgeValidity,
63
+ KnowledgeVerification,
64
+ VALIDITY_STATES,
65
+ )
66
+ from .output_layout import (
67
+ DEFAULT_OUTPUT_ROOT,
68
+ OutputLayout,
69
+ USER_OUTPUT_CONFIG_PATH,
70
+ deep_merge,
71
+ load_output_config,
72
+ write_output_config,
73
+ )
74
+
75
+ __all__ = [
76
+ "AgentResult",
77
+ "BUDGET_POLICY_SCHEMA_VERSION",
78
+ "BudgetExhausted",
79
+ "BudgetPolicy",
80
+ "BudgetReservation",
81
+ "InvestigationCacheStore",
82
+ "InvestigationCompareError",
83
+ "InvestigationCompareLimits",
84
+ "EvidencePointer",
85
+ "FINDING_OUTCOMES",
86
+ "HYPOTHESIS_STATUSES",
87
+ "INVESTIGATION_SCHEMA_VERSION",
88
+ "INVESTIGATION_STATUSES",
89
+ "STOP_KINDS",
90
+ "InvestigationError",
91
+ "InvestigationState",
92
+ "InvestigationStore",
93
+ "InvestigationSummaryError",
94
+ "RESULT_OUTCOMES",
95
+ "RESULT_SCHEMA_VERSION",
96
+ "SCENARIO_SCHEMA_VERSION",
97
+ "ScenarioDocument",
98
+ "ScenarioError",
99
+ "ScenarioProfile",
100
+ "ScenarioRuntime",
101
+ "SUMMARY_SCHEMA_VERSION",
102
+ "COMPARE_SCHEMA_VERSION",
103
+ "TIMELINE_SCHEMA_VERSION",
104
+ "SummaryLimits",
105
+ "GovernancePolicy",
106
+ "KnowledgeCandidate",
107
+ "KnowledgeGovernanceError",
108
+ "KnowledgeGovernanceStore",
109
+ "KnowledgeValidity",
110
+ "KnowledgeVerification",
111
+ "VALIDITY_STATES",
112
+ "DEFAULT_OUTPUT_ROOT",
113
+ "OutputLayout",
114
+ "USER_OUTPUT_CONFIG_PATH",
115
+ "deep_merge",
116
+ "load_output_config",
117
+ "write_output_config",
118
+ "probe",
119
+ "sample",
120
+ "peek",
121
+ "search",
122
+ "survey",
123
+ "expand",
124
+ "attach_investigation_result",
125
+ "create_investigation",
126
+ "load_investigation",
127
+ "investigation_summary",
128
+ "propose_candidate",
129
+ "propose_knowledge_candidate",
130
+ "summarize_investigation",
131
+ "compare_investigation",
132
+ "compare_investigations",
133
+ "investigation_timeline",
134
+ "timeline_investigation",
135
+ "verify",
136
+ "run",
137
+ ]
@@ -0,0 +1,7 @@
1
+ """Public Core compatibility surface under the main :mod:`tracecite` package."""
2
+
3
+ from tracecite_core import * # noqa: F401,F403
4
+ from tracecite_core import __version__
5
+ from tracecite_core import __all__ as _CORE_ALL
6
+
7
+ __all__ = [*_CORE_ALL, "__version__"]
@@ -0,0 +1,104 @@
1
+ """Versioned extension contract for third-party TraceCite capabilities.
2
+
3
+ Extensions contribute capability registrations and domain semantics. Runtime
4
+ continues to own execution, evidence schemas, verification, budgets, and safety
5
+ gates. Entry-point loading is explicit; importing :mod:`tracecite` never runs
6
+ third-party registration code.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from tracecite_core.plugin_sdk import PluginAPI, load_entrypoint_plugins, loaded_plugins
15
+
16
+ from tracecite.runtime.assertions import register_assertion_type
17
+ from tracecite.runtime.reporting import register_report_outputter
18
+ from tracecite.runtime.runtime import DEFAULT_RUNTIME, ScenarioRuntime
19
+
20
+
21
+ EXTENSION_API_VERSION = "1"
22
+ _RUNTIMES: Dict[str, ScenarioRuntime] = {"default": DEFAULT_RUNTIME}
23
+
24
+
25
+ class ExtensionError(RuntimeError):
26
+ """An extension registration or lookup failed."""
27
+
28
+
29
+ def register_runtime(
30
+ name: str,
31
+ runtime: ScenarioRuntime,
32
+ *,
33
+ replace: bool = False,
34
+ ) -> None:
35
+ """Register a named domain runtime without changing TraceCite source."""
36
+ key = str(name).strip().lower()
37
+ if not key:
38
+ raise ExtensionError("runtime 名不能为空")
39
+ if not isinstance(runtime, ScenarioRuntime):
40
+ raise ExtensionError("runtime 必须是 ScenarioRuntime")
41
+ current = _RUNTIMES.get(key)
42
+ if current is not None and current is not runtime and not replace:
43
+ raise ExtensionError(f"runtime {key!r} 已注册")
44
+ _RUNTIMES[key] = runtime
45
+
46
+
47
+ def get_runtime(name: str = "default") -> ScenarioRuntime:
48
+ key = str(name).strip().lower() or "default"
49
+ try:
50
+ return _RUNTIMES[key]
51
+ except KeyError as exc:
52
+ known = ", ".join(available_runtimes())
53
+ raise ExtensionError(f"未知 runtime {key!r}(可用: {known})") from exc
54
+
55
+
56
+ def available_runtimes() -> List[str]:
57
+ return sorted(_RUNTIMES)
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ExtensionAPI(PluginAPI):
62
+ """Stable registration surface passed to installed domain extensions."""
63
+
64
+ version: str = EXTENSION_API_VERSION
65
+
66
+ def register_assertion_type(
67
+ self, name: str, evaluator: Any, *, replace: bool = False
68
+ ) -> None:
69
+ register_assertion_type(name, evaluator, replace=replace)
70
+
71
+ def register_report_outputter(
72
+ self, name: str, outputter: Any, *, replace: bool = False
73
+ ) -> None:
74
+ register_report_outputter(name, outputter, replace=replace)
75
+
76
+ def register_runtime(
77
+ self, name: str, runtime: ScenarioRuntime, *, replace: bool = False
78
+ ) -> None:
79
+ register_runtime(name, runtime, replace=replace)
80
+
81
+
82
+ def load_extensions(*, strict: bool = True) -> List[Dict[str, Optional[str]]]:
83
+ """Explicitly discover installed Core and Runtime extensions."""
84
+ return [
85
+ *load_entrypoint_plugins(group="tracecite.core.plugins", strict=strict),
86
+ *load_entrypoint_plugins(
87
+ group="tracecite.extensions",
88
+ strict=strict,
89
+ api=ExtensionAPI(),
90
+ version_attribute="TRACECITE_EXTENSION_API",
91
+ ),
92
+ ]
93
+
94
+
95
+ __all__ = [
96
+ "EXTENSION_API_VERSION",
97
+ "ExtensionAPI",
98
+ "ExtensionError",
99
+ "register_runtime",
100
+ "get_runtime",
101
+ "available_runtimes",
102
+ "load_extensions",
103
+ "loaded_plugins",
104
+ ]
@@ -0,0 +1,3 @@
1
+ """Adapters exposing TraceCite Runtime to external Agent hosts."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,195 @@
1
+ """Capability-selected, lossless Agent transport profiles.
2
+
3
+ Profiles are integration-only views over canonical Runtime Results. They never
4
+ change evidence, artifacts, or Runtime schemas; an unsupported capability
5
+ falls back to a portable JSON profile rather than silently dropping evidence.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Literal, Mapping
12
+
13
+
14
+ TransportFormat = Literal["canonical-json", "columnar-json", "frame"]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class AgentCapabilities:
19
+ """Capabilities declared by one selected Agent host, not a model name."""
20
+
21
+ stateful_history: bool = False
22
+ batch_expand: bool = True
23
+ text_frame: bool = False
24
+ strict_json: bool = False
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class AgentProfile:
29
+ """Token transport policy for one selected analysis Agent."""
30
+
31
+ name: str
32
+ transport: TransportFormat
33
+ requires_ledger: bool = False
34
+ compact_history: bool = False
35
+ requires: AgentCapabilities = AgentCapabilities()
36
+
37
+ def supports(self, capabilities: AgentCapabilities) -> bool:
38
+ return all(
39
+ not required or available
40
+ for required, available in (
41
+ (self.requires.stateful_history, capabilities.stateful_history),
42
+ (self.requires.batch_expand, capabilities.batch_expand),
43
+ (self.requires.text_frame, capabilities.text_frame),
44
+ (self.requires.strict_json, capabilities.strict_json),
45
+ )
46
+ )
47
+
48
+
49
+ _PROFILES: dict[str, AgentProfile] = {
50
+ "canonical": AgentProfile("canonical", "canonical-json"),
51
+ "agent": AgentProfile("agent", "columnar-json"),
52
+ "portable-json": AgentProfile("portable-json", "columnar-json"),
53
+ "strict-json": AgentProfile(
54
+ "strict-json",
55
+ "columnar-json",
56
+ requires=AgentCapabilities(strict_json=True),
57
+ ),
58
+ "stateful-index": AgentProfile(
59
+ "stateful-index",
60
+ "columnar-json",
61
+ requires_ledger=True,
62
+ compact_history=True,
63
+ requires=AgentCapabilities(stateful_history=True, batch_expand=True),
64
+ ),
65
+ "frame": AgentProfile(
66
+ "frame",
67
+ "frame",
68
+ requires_ledger=True,
69
+ compact_history=True,
70
+ requires=AgentCapabilities(
71
+ stateful_history=True,
72
+ batch_expand=True,
73
+ text_frame=True,
74
+ ),
75
+ ),
76
+ }
77
+
78
+
79
+ def profile_names() -> tuple[str, ...]:
80
+ """Return stable built-in profile names for host configuration."""
81
+
82
+ return tuple(_PROFILES)
83
+
84
+
85
+ def get_agent_profile(name: str) -> AgentProfile:
86
+ """Return a named built-in profile or fail with valid choices."""
87
+
88
+ try:
89
+ return _PROFILES[name]
90
+ except KeyError as exc:
91
+ choices = ", ".join(profile_names())
92
+ raise ValueError(f"unknown agent profile {name!r}; choose one of: {choices}") from exc
93
+
94
+
95
+ def select_agent_profile(
96
+ name: str,
97
+ capabilities: AgentCapabilities | None = None,
98
+ ) -> AgentProfile:
99
+ """Resolve ``auto`` safely or validate an explicitly selected profile."""
100
+
101
+ available = capabilities or AgentCapabilities()
102
+ if name == "auto":
103
+ if _PROFILES["stateful-index"].supports(available):
104
+ return _PROFILES["stateful-index"]
105
+ return _PROFILES["agent"]
106
+ if name == "portable-json":
107
+ return _PROFILES["portable-json"]
108
+ if name == "agent":
109
+ return _PROFILES["agent"]
110
+ profile = get_agent_profile(name)
111
+ if not profile.supports(available):
112
+ raise ValueError(
113
+ f"agent profile {name!r} requires capabilities not declared by this host"
114
+ )
115
+ return profile
116
+
117
+
118
+ def _clean_cell(value: Any) -> str:
119
+ return str(value if value is not None else "").replace("\t", " ").replace("\n", " ")
120
+
121
+
122
+ def render_frame(payload: Mapping[str, Any]) -> str:
123
+ """Render a compact search or expand-many view as a readable TCF frame.
124
+
125
+ The frame is a transport encoding, not a new canonical schema. It carries
126
+ the same columnar rows and Coverage as the JSON view, and can always fall
127
+ back to JSON for hosts that do not declare ``text_frame`` support.
128
+ """
129
+
130
+ operation = str(payload.get("operation") or "unknown")
131
+ status = str(payload.get("status") or "error")
132
+ outcome = str(payload.get("outcome") or "unknown")
133
+ lines = [f"@TCF 1 {operation} status={status} outcome={outcome}"]
134
+ result_id = str((payload.get("data") or {}).get("result_id") or payload.get("result_id") or "")
135
+ if result_id:
136
+ lines.append(f"@R {result_id}")
137
+
138
+ source = (payload.get("data") or {}).get("evidence_source") or {}
139
+ if isinstance(source, Mapping) and source.get("uri_base"):
140
+ lines.append(f"@SRC {_clean_cell(source['uri_base'])}")
141
+
142
+ coverage = payload.get("coverage") or {}
143
+ if isinstance(coverage, Mapping):
144
+ scalar_coverage = [
145
+ f"{key}={_clean_cell(value)}"
146
+ for key, value in sorted(coverage.items())
147
+ if not isinstance(value, (list, dict))
148
+ ]
149
+ if scalar_coverage:
150
+ lines.append("@COV " + " ".join(scalar_coverage))
151
+
152
+ evidence = payload.get("evidence") or {}
153
+ if isinstance(evidence, Mapping):
154
+ columns = evidence.get("columns") or []
155
+ rows = evidence.get("rows") or []
156
+ if columns:
157
+ lines.append("@E " + "\t".join(_clean_cell(column) for column in columns))
158
+ for row in rows:
159
+ if isinstance(row, list):
160
+ lines.append("\t".join(_clean_cell(cell) for cell in row))
161
+
162
+ contexts = payload.get("contexts") or []
163
+ if isinstance(contexts, list):
164
+ for context in contexts:
165
+ if not isinstance(context, Mapping):
166
+ continue
167
+ context_id = _clean_cell(context.get("id"))
168
+ context_lines = context.get("lines") or []
169
+ start = _clean_cell(context_lines[0] if len(context_lines) > 0 else "")
170
+ end = _clean_cell(context_lines[1] if len(context_lines) > 1 else "")
171
+ truncated = _clean_cell(context.get("truncated", False))
172
+ lines.append(f"@CTX {context_id} {start}-{end} truncated={truncated}")
173
+ lines.extend(str(context.get("text") or "").splitlines())
174
+ lines.append("@ENDCTX")
175
+
176
+ warnings = payload.get("warnings") or []
177
+ for warning in warnings:
178
+ lines.append("@WARN " + _clean_cell(warning))
179
+ error = payload.get("error") or {}
180
+ if isinstance(error, Mapping):
181
+ message = error.get("message")
182
+ if message:
183
+ lines.append("@ERR " + _clean_cell(message))
184
+ return "\n".join(lines)
185
+
186
+
187
+ __all__ = [
188
+ "AgentCapabilities",
189
+ "AgentProfile",
190
+ "TransportFormat",
191
+ "get_agent_profile",
192
+ "profile_names",
193
+ "render_frame",
194
+ "select_agent_profile",
195
+ ]
@@ -0,0 +1,173 @@
1
+ """Shared agent-facing projections over canonical tool results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import json
7
+ from typing import Any, Mapping
8
+
9
+ DEFAULT_AGENT_MAX_OUTPUT_CHARS = 12_000
10
+ DEFAULT_FILTER_MAX_LINE_CHARS = 1024
11
+ DEFAULT_AGENT_MAX_EVIDENCE = 30
12
+
13
+
14
+ def encoded_json(payload: Any) -> str:
15
+ return json.dumps(
16
+ payload,
17
+ ensure_ascii=False,
18
+ sort_keys=True,
19
+ separators=(",", ":"),
20
+ )
21
+
22
+
23
+ def dedupe_survey_coverage(coverage: Mapping[str, Any]) -> dict[str, Any]:
24
+ """Collapse synonymous survey coverage keys for agent transport."""
25
+
26
+ aliases = {
27
+ "scanned_lines": "lines_scanned",
28
+ "scanned_records": "records_scanned",
29
+ "records_scoped": "scoped_records",
30
+ "lines_scoped": "scoped_lines",
31
+ }
32
+ deduped: dict[str, Any] = {}
33
+ for key, value in coverage.items():
34
+ canonical = aliases.get(key, key)
35
+ if canonical not in deduped:
36
+ deduped[canonical] = value
37
+ return deduped
38
+
39
+
40
+ def apply_survey_brief(payload: Mapping[str, Any]) -> dict[str, Any]:
41
+ """Project a canonical survey Result into a token-efficient agent view."""
42
+
43
+ result = copy.deepcopy(dict(payload))
44
+ if result.get("operation") != "survey":
45
+ return result
46
+
47
+ data = dict(result.get("data") or {})
48
+ data["brief"] = True
49
+ for template in data.get("top_templates") or []:
50
+ if not isinstance(template, Mapping):
51
+ continue
52
+ for sample in template.get("samples") or []:
53
+ if isinstance(sample, Mapping):
54
+ sample.pop("text", None)
55
+ for key in ("work_input", "snapshot_path"):
56
+ data.pop(key, None)
57
+ result["data"] = data
58
+
59
+ evidence = []
60
+ for item in result.get("evidence") or []:
61
+ if not isinstance(item, Mapping):
62
+ continue
63
+ row = dict(item)
64
+ metadata = dict(row.get("metadata") or {})
65
+ metadata.pop("text", None)
66
+ if metadata:
67
+ row["metadata"] = metadata
68
+ else:
69
+ row.pop("metadata", None)
70
+ label = str(row.get("label") or "")
71
+ if label:
72
+ row["label"] = label[:80]
73
+ evidence.append(row)
74
+ result["evidence"] = evidence
75
+ result["coverage"] = dedupe_survey_coverage(result.get("coverage") or {})
76
+ return result
77
+
78
+
79
+ def dedupe_evidence_labels(
80
+ evidence_rows: list[list[Any]],
81
+ *,
82
+ label_index: int,
83
+ coverage: dict[str, Any],
84
+ ) -> None:
85
+ """Hoist or omit repeated search labels inside compact evidence rows."""
86
+
87
+ labels = [
88
+ str(row[label_index])
89
+ for row in evidence_rows
90
+ if label_index < len(row) and row[label_index]
91
+ ]
92
+ if not labels:
93
+ return
94
+ unique = set(labels)
95
+ if len(unique) == 1:
96
+ coverage["shared_label"] = labels[0]
97
+ for row in evidence_rows:
98
+ if label_index < len(row):
99
+ row[label_index] = ""
100
+ return
101
+ previous = None
102
+ for row in evidence_rows:
103
+ if label_index >= len(row):
104
+ continue
105
+ current = row[label_index]
106
+ if current and current == previous:
107
+ row[label_index] = ""
108
+ elif current:
109
+ previous = current
110
+
111
+
112
+ def lightweight_result(payload: Mapping[str, Any]) -> dict[str, Any]:
113
+ """Drop empty investigation envelope fields from agent transport."""
114
+
115
+ result = copy.deepcopy(dict(payload))
116
+ for key in ("hypotheses", "verification"):
117
+ if not result.get(key):
118
+ result.pop(key, None)
119
+ artifacts = result.get("artifacts") or []
120
+ if not artifacts:
121
+ result.pop("artifacts", None)
122
+ data = dict(result.get("data") or {})
123
+ for key in ("run_id", "manifest_path", "manifest_sha256", "input_lineage"):
124
+ data.pop(key, None)
125
+ if data:
126
+ result["data"] = data
127
+ else:
128
+ result.pop("data", None)
129
+ return result
130
+
131
+
132
+ def compact_filter_payload(payload: Mapping[str, Any]) -> dict[str, Any]:
133
+ """Return a bounded mobile filter view; full text stays in records_path."""
134
+
135
+ source = dict(payload)
136
+ keep = (
137
+ "match_records",
138
+ "match_lines",
139
+ "scope",
140
+ "time_from",
141
+ "time_to",
142
+ "tag",
143
+ "pattern",
144
+ "records_path",
145
+ "hits_path",
146
+ "templates_path",
147
+ "unmatched_summary",
148
+ "term_usage",
149
+ "lines_truncated",
150
+ "max_line_chars",
151
+ )
152
+ view = {key: source[key] for key in keep if key in source and source[key] is not None}
153
+ view["view"] = "agent"
154
+ view["recovery"] = (
155
+ "Do not Read output_path directly. Use records_path with "
156
+ "tracecite-core search --compact or expand for full lines."
157
+ )
158
+ if source.get("output_path"):
159
+ view["output_path"] = source["output_path"]
160
+ return view
161
+
162
+
163
+ __all__ = [
164
+ "DEFAULT_AGENT_MAX_EVIDENCE",
165
+ "DEFAULT_AGENT_MAX_OUTPUT_CHARS",
166
+ "DEFAULT_FILTER_MAX_LINE_CHARS",
167
+ "apply_survey_brief",
168
+ "compact_filter_payload",
169
+ "dedupe_evidence_labels",
170
+ "dedupe_survey_coverage",
171
+ "encoded_json",
172
+ "lightweight_result",
173
+ ]