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.
- tracecite/__init__.py +137 -0
- tracecite/core/__init__.py +7 -0
- tracecite/extension/__init__.py +104 -0
- tracecite/integrations/__init__.py +3 -0
- tracecite/integrations/agent_profile.py +195 -0
- tracecite/integrations/agent_projection.py +173 -0
- tracecite/integrations/cli.py +1225 -0
- tracecite/integrations/evidence_ledger.py +293 -0
- tracecite/knowledge/__init__.py +1286 -0
- tracecite/output_layout.py +77 -0
- tracecite/runtime/__init__.py +157 -0
- tracecite/runtime/assertions.py +358 -0
- tracecite/runtime/cli.py +9 -0
- tracecite/runtime/investigation.py +2735 -0
- tracecite/runtime/investigation_compare.py +902 -0
- tracecite/runtime/investigation_summary.py +826 -0
- tracecite/runtime/reporting.py +168 -0
- tracecite/runtime/runtime.py +161 -0
- tracecite/runtime/scenario.py +1940 -0
- tracecite/runtime/schema.py +273 -0
- tracecite/runtime/schema_compat.py +602 -0
- tracecite/runtime/tools.py +1463 -0
- tracecite-0.1.0.dist-info/METADATA +159 -0
- tracecite-0.1.0.dist-info/RECORD +48 -0
- tracecite-0.1.0.dist-info/WHEEL +5 -0
- tracecite-0.1.0.dist-info/entry_points.txt +3 -0
- tracecite-0.1.0.dist-info/licenses/LICENSE +22 -0
- tracecite-0.1.0.dist-info/top_level.txt +2 -0
- tracecite_core/__init__.py +158 -0
- tracecite_core/cli.py +139 -0
- tracecite_core/events.py +257 -0
- tracecite_core/format_probe.py +629 -0
- tracecite_core/immutable.py +19 -0
- tracecite_core/live_cut.py +150 -0
- tracecite_core/log_filter.py +2 -0
- tracecite_core/matcher.py +745 -0
- tracecite_core/output_layout.py +84 -0
- tracecite_core/plugin_sdk.py +164 -0
- tracecite_core/preprocess.py +121 -0
- tracecite_core/records.py +63 -0
- tracecite_core/run.py +379 -0
- tracecite_core/sample.py +555 -0
- tracecite_core/segment_store.py +157 -0
- tracecite_core/segmenter.py +597 -0
- tracecite_core/source.py +557 -0
- tracecite_core/state_file.py +66 -0
- tracecite_core/survey.py +605 -0
- 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,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,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
|
+
]
|