atif-sql 0.2.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.
- atif_analytics/__init__.py +9 -0
- atif_analytics/application/__init__.py +3 -0
- atif_analytics/application/analyze.py +158 -0
- atif_analytics/application/prompts.py +750 -0
- atif_analytics/application/use_cases/__init__.py +3 -0
- atif_analytics/application/use_cases/_shared.py +264 -0
- atif_analytics/application/use_cases/classify.py +430 -0
- atif_analytics/application/use_cases/conflicts.py +448 -0
- atif_analytics/application/use_cases/friction.py +634 -0
- atif_analytics/application/use_cases/perceived.py +451 -0
- atif_analytics/domain/__init__.py +3 -0
- atif_analytics/domain/authorship.py +166 -0
- atif_analytics/domain/config.py +30 -0
- atif_analytics/domain/costs.py +50 -0
- atif_analytics/domain/friction.py +117 -0
- atif_analytics/domain/layout.py +94 -0
- atif_analytics/domain/models.py +385 -0
- atif_analytics/domain/ports.py +100 -0
- atif_analytics/domain/transcript.py +359 -0
- atif_analytics/infrastructure/__init__.py +3 -0
- atif_analytics/infrastructure/corpus_reader.py +546 -0
- atif_analytics/infrastructure/parquet_cache.py +186 -0
- atif_analytics/infrastructure/settings.py +114 -0
- atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
- atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
- atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
- atif_analytics/py.typed +0 -0
- atif_cli/__init__.py +3 -0
- atif_cli/__main__.py +8 -0
- atif_cli/app.py +2459 -0
- atif_cli/converter_adapter.py +151 -0
- atif_cli/corpus.py +409 -0
- atif_cli/cron.py +311 -0
- atif_cli/duck_errors.py +97 -0
- atif_cli/embed_lake.py +101 -0
- atif_cli/errors.py +89 -0
- atif_cli/lake.py +373 -0
- atif_cli/lake_sessions.py +270 -0
- atif_cli/output.py +297 -0
- atif_cli/py.typed +0 -0
- atif_converter/__init__.py +3 -0
- atif_converter/application/__init__.py +3 -0
- atif_converter/application/convert_and_audit.py +294 -0
- atif_converter/application/convert_codex.py +210 -0
- atif_converter/domain/__init__.py +9 -0
- atif_converter/domain/agents.py +40 -0
- atif_converter/domain/atif/__init__.py +55 -0
- atif_converter/domain/atif/agent.py +37 -0
- atif_converter/domain/atif/content.py +161 -0
- atif_converter/domain/atif/final_metrics.py +45 -0
- atif_converter/domain/atif/metrics.py +49 -0
- atif_converter/domain/atif/observation.py +21 -0
- atif_converter/domain/atif/observation_result.py +47 -0
- atif_converter/domain/atif/step.py +144 -0
- atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
- atif_converter/domain/atif/tool_call.py +36 -0
- atif_converter/domain/atif/trajectory.py +211 -0
- atif_converter/domain/atif/trajectory_validator.py +299 -0
- atif_converter/domain/blobs.py +510 -0
- atif_converter/domain/claude_code_conversion.py +1077 -0
- atif_converter/domain/codex_conversion.py +1036 -0
- atif_converter/domain/codex_edges.py +192 -0
- atif_converter/domain/codex_enrichment.py +419 -0
- atif_converter/domain/codex_fidelity.py +124 -0
- atif_converter/domain/codex_nested_calls.py +165 -0
- atif_converter/domain/edges.py +113 -0
- atif_converter/domain/enrichment.py +412 -0
- atif_converter/domain/errors.py +60 -0
- atif_converter/domain/fidelity.py +171 -0
- atif_converter/domain/model_prices.json +8538 -0
- atif_converter/domain/pricing.py +790 -0
- atif_converter/domain/result_signals.py +774 -0
- atif_converter/domain/schema_version.py +90 -0
- atif_converter/domain/session_events.py +408 -0
- atif_converter/infrastructure/__init__.py +3 -0
- atif_converter/infrastructure/census.py +89 -0
- atif_converter/infrastructure/claude_code_converter.py +127 -0
- atif_converter/infrastructure/codex_adapter.py +132 -0
- atif_converter/infrastructure/codex_census.py +100 -0
- atif_converter/infrastructure/codex_converter.py +82 -0
- atif_converter/infrastructure/harbor_adapter.py +187 -0
- atif_converter/infrastructure/raw_records.py +426 -0
- atif_converter/infrastructure/source_archive.py +154 -0
- atif_converter/py.typed +0 -0
- atif_corpus/__init__.py +13 -0
- atif_corpus/application/__init__.py +7 -0
- atif_corpus/application/materialize.py +1826 -0
- atif_corpus/domain/__init__.py +9 -0
- atif_corpus/domain/agents.py +33 -0
- atif_corpus/domain/generation.py +34 -0
- atif_corpus/domain/layout.py +168 -0
- atif_corpus/domain/ports.py +175 -0
- atif_corpus/domain/session_id.py +68 -0
- atif_corpus/domain/sessions.py +228 -0
- atif_corpus/domain/slug.py +72 -0
- atif_corpus/domain/source_layout.py +143 -0
- atif_corpus/domain/watermark.py +76 -0
- atif_corpus/infrastructure/__init__.py +7 -0
- atif_corpus/infrastructure/atomic.py +266 -0
- atif_corpus/infrastructure/compress_artifacts.py +161 -0
- atif_corpus/infrastructure/fake_converter.py +174 -0
- atif_corpus/infrastructure/scanner.py +316 -0
- atif_corpus/infrastructure/settings.py +130 -0
- atif_corpus/infrastructure/source_archive.py +202 -0
- atif_corpus/py.typed +0 -0
- atif_duck/__init__.py +43 -0
- atif_duck/domain/__init__.py +32 -0
- atif_duck/domain/artifacts.py +66 -0
- atif_duck/domain/authorship.py +165 -0
- atif_duck/domain/catalog.py +626 -0
- atif_duck/domain/columnar.py +149 -0
- atif_duck/domain/embedding_guard.py +88 -0
- atif_duck/domain/examples.py +265 -0
- atif_duck/domain/lake.py +427 -0
- atif_duck/domain/lake_sessions.py +97 -0
- atif_duck/domain/raw_readers.py +140 -0
- atif_duck/domain/session_id.py +69 -0
- atif_duck/domain/sql_literal.py +50 -0
- atif_duck/infrastructure/__init__.py +34 -0
- atif_duck/infrastructure/analytics.py +469 -0
- atif_duck/infrastructure/authorship.py +139 -0
- atif_duck/infrastructure/columnar.py +641 -0
- atif_duck/infrastructure/lake.py +1383 -0
- atif_duck/infrastructure/lake_sessions.py +194 -0
- atif_duck/infrastructure/lake_settings.py +63 -0
- atif_duck/infrastructure/lake_steps.py +225 -0
- atif_duck/infrastructure/projections.py +221 -0
- atif_duck/infrastructure/registry.py +2194 -0
- atif_duck/infrastructure/stored_artifacts.py +115 -0
- atif_duck/py.typed +0 -0
- atif_embed/__init__.py +12 -0
- atif_embed/application/__init__.py +3 -0
- atif_embed/application/embed.py +293 -0
- atif_embed/application/prune.py +80 -0
- atif_embed/domain/__init__.py +3 -0
- atif_embed/domain/discovery.py +70 -0
- atif_embed/domain/embedding_guard.py +83 -0
- atif_embed/domain/errors.py +94 -0
- atif_embed/domain/ports.py +223 -0
- atif_embed/domain/sql_literal.py +49 -0
- atif_embed/domain/text_stamp.py +71 -0
- atif_embed/infrastructure/__init__.py +3 -0
- atif_embed/infrastructure/cohere_bedrock.py +433 -0
- atif_embed/infrastructure/corpus_text_rows.py +273 -0
- atif_embed/infrastructure/gemma_local.py +355 -0
- atif_embed/infrastructure/lake_text_rows.py +203 -0
- atif_embed/infrastructure/lance_store.py +440 -0
- atif_embed/infrastructure/providers.py +30 -0
- atif_embed/infrastructure/settings.py +154 -0
- atif_embed/py.typed +0 -0
- atif_models/__init__.py +14 -0
- atif_models/domain/__init__.py +9 -0
- atif_models/domain/ports.py +143 -0
- atif_models/domain/registry.py +151 -0
- atif_models/domain/schema.py +109 -0
- atif_models/infrastructure/__init__.py +8 -0
- atif_models/infrastructure/openai_bedrock.py +314 -0
- atif_models/infrastructure/settings.py +85 -0
- atif_models/py.typed +0 -0
- atif_sql-0.2.0.dist-info/METADATA +272 -0
- atif_sql-0.2.0.dist-info/RECORD +164 -0
- atif_sql-0.2.0.dist-info/WHEEL +4 -0
- atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
- atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""The fidelity policy for Codex CLI: what harbor's Codex converter loses.
|
|
4
|
+
|
|
5
|
+
Pure value objects — no harbor import, no I/O. The Claude Code half of this
|
|
6
|
+
policy lives in :mod:`atif_converter.domain.fidelity`; this module is its
|
|
7
|
+
Codex counterpart and follows the same discipline: NAME each upstream gap
|
|
8
|
+
rather than fix it, and let the unit tests pin it so a harbor bump that
|
|
9
|
+
changes behavior trips the suite.
|
|
10
|
+
|
|
11
|
+
Every member below was verified empirically on 2026-09-11 against
|
|
12
|
+
harbor==0.22.0's Codex conversion over real rollouts from ``~/.codex/sessions``
|
|
13
|
+
written by codex-cli 0.153.4 and 0.154.0. Our converter
|
|
14
|
+
(:mod:`atif_converter.domain.codex_conversion`) is a parity port of that
|
|
15
|
+
conversion, so these losses are ours now, by choice.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from enum import Enum
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CodexRecordType(Enum):
|
|
24
|
+
"""Taxonomy of raw Codex rollout JSONL record types.
|
|
25
|
+
|
|
26
|
+
The top-level ``type`` field values seen in the wild, plus ``OTHER`` for
|
|
27
|
+
anything new. Only ``RESPONSE_ITEM`` records become ATIF steps;
|
|
28
|
+
``EVENT_MSG`` and ``TURN_CONTEXT`` are read for turn ids and token
|
|
29
|
+
counts but produce no step of their own, and the rest are dropped
|
|
30
|
+
(:attr:`CodexFidelityGap.NON_ITEM_RECORDS_DROPPED`).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
SESSION_META = "session_meta"
|
|
34
|
+
RESPONSE_ITEM = "response_item"
|
|
35
|
+
EVENT_MSG = "event_msg"
|
|
36
|
+
TURN_CONTEXT = "turn_context"
|
|
37
|
+
TOKEN_USAGE_RECORD = "token_usage_record" # noqa: S105 — a record type, not a secret
|
|
38
|
+
WORLD_STATE = "world_state"
|
|
39
|
+
COMPACTED = "compacted"
|
|
40
|
+
OTHER = "other"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#: Record types harbor 0.22.0 turns into ATIF steps. ``EVENT_MSG`` and
|
|
44
|
+
#: ``TURN_CONTEXT`` are deliberately absent: the converter reads them for
|
|
45
|
+
#: ``token_count`` metrics and turn ids, and emits no step for either, so
|
|
46
|
+
#: counting them as converted would overstate what the trajectory carries.
|
|
47
|
+
CODEX_CONVERTIBLE_RECORD_TYPES: frozenset[CodexRecordType] = frozenset(
|
|
48
|
+
{CodexRecordType.RESPONSE_ITEM}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
#: ``response_item`` payload types that DO reach a step, so a rollout whose
|
|
52
|
+
#: response items are all of some other type is honestly reported as
|
|
53
|
+
#: converting none of them.
|
|
54
|
+
CODEX_CONVERTIBLE_ITEM_TYPES: frozenset[str] = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"message",
|
|
57
|
+
"function_call",
|
|
58
|
+
"function_call_output",
|
|
59
|
+
"custom_tool_call",
|
|
60
|
+
"custom_tool_call_output",
|
|
61
|
+
"web_search_call",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CodexFidelityGap(Enum):
|
|
67
|
+
"""The known Codex conversion gaps, inherited from harbor 0.22.0.
|
|
68
|
+
|
|
69
|
+
Values are namespaced ``codex_*`` because they share the
|
|
70
|
+
``gaps_observed`` array in ``loss_report.json`` with the Claude Code
|
|
71
|
+
gaps: a reader of one corpus must be able to tell which policy a gap
|
|
72
|
+
came from without consulting a second column.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
#: (1) RETIRED. ``codex_single_rollout_per_directory`` named harbor's
|
|
76
|
+
#: directory globbing, which converted only ``max(*.jsonl)`` of a session
|
|
77
|
+
#: dir. Our converter reads the one rollout it is given, so there is no
|
|
78
|
+
#: directory and no gap. The value is not reused; a corpus materialized
|
|
79
|
+
#: before the port still carries it.
|
|
80
|
+
|
|
81
|
+
#: (2) Only ``response_item`` records become steps. ``world_state``,
|
|
82
|
+
#: ``token_usage_record`` and ``compacted`` records are dropped outright,
|
|
83
|
+
#: and ``event_msg`` / ``turn_context`` contribute only turn ids and token
|
|
84
|
+
#: counts. Verified: a 17-record rollout yields 7 steps.
|
|
85
|
+
NON_ITEM_RECORDS_DROPPED = "codex_non_item_records_dropped"
|
|
86
|
+
|
|
87
|
+
#: (3) ``compacted`` records carry the compaction summary plus the
|
|
88
|
+
#: ``replacement_history`` the model actually saw afterwards. Neither
|
|
89
|
+
#: reaches the trajectory, so a compaction boundary is invisible in the
|
|
90
|
+
#: step list and the pre-compaction turns simply stop.
|
|
91
|
+
COMPACTION_UNHANDLED = "codex_compaction_unhandled"
|
|
92
|
+
|
|
93
|
+
#: (4) Reasoning is read from ``reasoning.summary`` only. Recent Codex
|
|
94
|
+
#: builds ship an EMPTY summary and put the content in
|
|
95
|
+
#: ``encrypted_content``, so reasoning is lost entirely: verified on a
|
|
96
|
+
#: rollout with 4 reasoning records and 0 steps carrying
|
|
97
|
+
#: ``reasoning_content``.
|
|
98
|
+
REASONING_ENCRYPTED_DROPPED = "codex_reasoning_encrypted_dropped"
|
|
99
|
+
|
|
100
|
+
#: (5) ``tool_search_call`` / ``tool_search_output`` items (Codex's
|
|
101
|
+
#: deferred-tool-loading search) match no branch of the payload-type
|
|
102
|
+
#: dispatch, so a tool search and its result vanish without a warning.
|
|
103
|
+
TOOL_SEARCH_CALLS_DROPPED = "codex_tool_search_calls_dropped"
|
|
104
|
+
|
|
105
|
+
#: (6) ``response_item.payload.id`` (``msg_*`` / ``call_*`` / ``rs_*``) is
|
|
106
|
+
#: never carried into ``Step.extra``, so step-to-raw-record identity is
|
|
107
|
+
#: unrecoverable from the trajectory alone — the same shape of loss as
|
|
108
|
+
#: Claude Code's gap 7, and repaired the same way by the enrichment pass.
|
|
109
|
+
ITEM_IDS_NOT_PRESERVED = "codex_item_ids_not_preserved"
|
|
110
|
+
|
|
111
|
+
#: (7) A ``developer`` role message becomes ATIF ``source="system"``,
|
|
112
|
+
#: which is also where a genuine system message lands. The two are
|
|
113
|
+
#: indistinguishable in the trajectory; ``edges.jsonl`` keeps the raw
|
|
114
|
+
#: role, so the distinction survives in the corpus but not in the steps.
|
|
115
|
+
DEVELOPER_ROLE_FLATTENED_TO_SYSTEM = "codex_developer_role_flattened_to_system"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
#: Gaps every harbor 0.22.0 Codex conversion exhibits regardless of rollout
|
|
119
|
+
#: content — the structural set, mirroring the Claude Code policy's.
|
|
120
|
+
CODEX_STRUCTURAL_GAPS: frozenset[CodexFidelityGap] = frozenset(
|
|
121
|
+
{
|
|
122
|
+
CodexFidelityGap.ITEM_IDS_NOT_PRESERVED,
|
|
123
|
+
}
|
|
124
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""A Codex code-mode script's nested MCP calls and commands, as ATIF tool calls.
|
|
4
|
+
|
|
5
|
+
In code mode Codex calls one ``exec`` tool whose JavaScript calls
|
|
6
|
+
``tools.mcp__<server>__<tool>(...)`` and ``tools.exec_command(...)``. The
|
|
7
|
+
rollout records each nested call as an ``event_msg`` ``item_completed`` item
|
|
8
|
+
(``McpToolCall`` or ``CommandExecution``, id ``exec-<uuid>``) between the
|
|
9
|
+
script's ``custom_tool_call`` and its ``custom_tool_call_output``, and harbor's
|
|
10
|
+
converter turns none of them into a tool call. Pure functions over one such
|
|
11
|
+
item: the :class:`~atif_converter.domain.atif.ToolCall` and
|
|
12
|
+
:class:`~atif_converter.domain.atif.ObservationResult` JSON it becomes. Which
|
|
13
|
+
script an item belongs to is :mod:`atif_converter.domain.result_signals`'s
|
|
14
|
+
attribution; that pass also writes the result's ``is_error`` / ``exit_code``.
|
|
15
|
+
|
|
16
|
+
The tool call
|
|
17
|
+
-------------
|
|
18
|
+
``tool_call_id``
|
|
19
|
+
The item's own ``id``.
|
|
20
|
+
``function_name``
|
|
21
|
+
``mcp__<server>__<tool>`` for an MCP call, the name the script called;
|
|
22
|
+
``exec_command`` for a command.
|
|
23
|
+
``arguments``
|
|
24
|
+
The MCP call's ``arguments`` object; ``{"cmd": <script>}`` for a command,
|
|
25
|
+
unwrapped from Codex's ``[<shell>, "-c" | "-lc", <script>]`` argv.
|
|
26
|
+
``extra``
|
|
27
|
+
``nested_in`` (the ``exec`` call's id), ``via`` (``"exec"``) and, when the
|
|
28
|
+
item states it, ``duration_ms``.
|
|
29
|
+
|
|
30
|
+
The result's content is the MCP result's content blocks rendered the way the
|
|
31
|
+
port renders a top-level tool output's blocks
|
|
32
|
+
(:func:`~atif_converter.domain.codex_conversion.structured_output_text`), or a
|
|
33
|
+
command's ``aggregated_output``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import shlex
|
|
40
|
+
from typing import Any, Final
|
|
41
|
+
|
|
42
|
+
from atif_converter.domain.codex_conversion import structured_output_text
|
|
43
|
+
|
|
44
|
+
#: Completed item types that become a nested tool call.
|
|
45
|
+
NESTED_CALL_ITEM_TYPES: Final[frozenset[str]] = frozenset({"McpToolCall", "CommandExecution"})
|
|
46
|
+
|
|
47
|
+
#: The ``function_name`` a nested command gets: the tool the script called.
|
|
48
|
+
COMMAND_TOOL_NAME: Final = "exec_command"
|
|
49
|
+
|
|
50
|
+
#: ``extra.via`` on every nested call: the tool that ran it.
|
|
51
|
+
VIA_EXEC: Final = "exec"
|
|
52
|
+
|
|
53
|
+
#: A shell's flags before the script in Codex's command argv.
|
|
54
|
+
_SHELL_SCRIPT_FLAGS: Final[frozenset[str]] = frozenset({"-c", "-lc"})
|
|
55
|
+
|
|
56
|
+
#: The length of a ``[<shell>, <flag>, <script>]`` argv.
|
|
57
|
+
_SHELL_ARGV_LENGTH: Final = 3
|
|
58
|
+
|
|
59
|
+
_NANOS_PER_MS: Final = 1_000_000
|
|
60
|
+
_MS_PER_SEC: Final = 1000
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _json_text(value: Any) -> str:
|
|
64
|
+
return json.dumps(value, ensure_ascii=False)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _arguments(value: Any) -> dict[str, Any]:
|
|
68
|
+
"""An MCP item's ``arguments`` as an object, by the port's rule for a call's arguments."""
|
|
69
|
+
if isinstance(value, str):
|
|
70
|
+
try:
|
|
71
|
+
value = json.loads(value)
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
return {"input": value}
|
|
74
|
+
if value is None:
|
|
75
|
+
return {}
|
|
76
|
+
return value if isinstance(value, dict) else {"value": value}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _command_text(command: Any) -> Any:
|
|
80
|
+
"""The script a command ran: unwrapped from its shell argv, else the argv as one line."""
|
|
81
|
+
if isinstance(command, list) and all(isinstance(part, str) for part in command):
|
|
82
|
+
if len(command) == _SHELL_ARGV_LENGTH and command[1] in _SHELL_SCRIPT_FLAGS:
|
|
83
|
+
return command[2]
|
|
84
|
+
return shlex.join(command)
|
|
85
|
+
return command
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _duration_ms(item: dict[str, Any], payload: dict[str, Any]) -> float | None:
|
|
89
|
+
"""The call's duration: the item's ``{secs, nanos}``, else the payload's two timestamps."""
|
|
90
|
+
duration = item.get("duration")
|
|
91
|
+
if isinstance(duration, dict):
|
|
92
|
+
secs, nanos = duration.get("secs"), duration.get("nanos")
|
|
93
|
+
if isinstance(secs, int) and isinstance(nanos, int):
|
|
94
|
+
return round(secs * _MS_PER_SEC + nanos / _NANOS_PER_MS, 3)
|
|
95
|
+
started, completed = payload.get("started_at_ms"), payload.get("completed_at_ms")
|
|
96
|
+
if isinstance(started, int) and isinstance(completed, int) and completed >= started:
|
|
97
|
+
return float(completed - started)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def nested_tool_call(payload: dict[str, Any], exec_call_id: str) -> dict[str, Any]:
|
|
102
|
+
"""The ``ToolCall`` JSON one ``item_completed`` payload's item becomes, nested in ``exec_call_id``."""
|
|
103
|
+
item: dict[str, Any] = payload["item"]
|
|
104
|
+
if item.get("type") == "CommandExecution":
|
|
105
|
+
function_name = COMMAND_TOOL_NAME
|
|
106
|
+
arguments: dict[str, Any] = {"cmd": _command_text(item.get("command"))}
|
|
107
|
+
else:
|
|
108
|
+
function_name = f"mcp__{item.get('server')}__{item.get('tool')}"
|
|
109
|
+
arguments = _arguments(item.get("arguments"))
|
|
110
|
+
extra: dict[str, Any] = {"nested_in": exec_call_id, "via": VIA_EXEC}
|
|
111
|
+
duration = _duration_ms(item, payload)
|
|
112
|
+
if duration is not None:
|
|
113
|
+
extra["duration_ms"] = duration
|
|
114
|
+
return {
|
|
115
|
+
"tool_call_id": item["id"],
|
|
116
|
+
"function_name": function_name,
|
|
117
|
+
"arguments": arguments,
|
|
118
|
+
"extra": extra,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _text(value: Any) -> str:
|
|
123
|
+
return value if isinstance(value, str) else _json_text(value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _result_text(result: dict[str, Any]) -> str:
|
|
127
|
+
"""An MCP result's text: its ``Err``, else its content blocks, else its JSON text."""
|
|
128
|
+
if "Err" in result:
|
|
129
|
+
return _text(result["Err"])
|
|
130
|
+
content = result.get("content")
|
|
131
|
+
if not isinstance(content, list):
|
|
132
|
+
return _json_text(result)
|
|
133
|
+
text = structured_output_text(content)
|
|
134
|
+
if text is not None:
|
|
135
|
+
return text
|
|
136
|
+
return _json_text(content) if content else ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _mcp_content(item: dict[str, Any]) -> str | None:
|
|
140
|
+
"""The result's text, else the error's message (an item that failed before any result)."""
|
|
141
|
+
result = item.get("result")
|
|
142
|
+
if isinstance(result, dict):
|
|
143
|
+
return _result_text(result)
|
|
144
|
+
error = item.get("error")
|
|
145
|
+
message = error.get("message") if isinstance(error, dict) else None
|
|
146
|
+
if isinstance(message, str):
|
|
147
|
+
return message
|
|
148
|
+
return None if error is None else _text(error)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def nested_result_content(item: dict[str, Any]) -> str | None:
|
|
152
|
+
"""The observation content of one nested item: its MCP result or command output."""
|
|
153
|
+
if item.get("type") == "CommandExecution":
|
|
154
|
+
output = item.get("aggregated_output")
|
|
155
|
+
return output if isinstance(output, str) else None
|
|
156
|
+
return _mcp_content(item)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"COMMAND_TOOL_NAME",
|
|
161
|
+
"NESTED_CALL_ITEM_TYPES",
|
|
162
|
+
"VIA_EXEC",
|
|
163
|
+
"nested_result_content",
|
|
164
|
+
"nested_tool_call",
|
|
165
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""edges.jsonl: one line per RAW session record — the uuid-keyed skeleton.
|
|
4
|
+
|
|
5
|
+
Pure functions over already-parsed record dicts; no harbor, no I/O. The
|
|
6
|
+
corpus contract (docs/CONTRACT.md §corpus layout) fixes the line shape:
|
|
7
|
+
|
|
8
|
+
{uuid, parent_uuid, message_id, type, ts, is_sidechain,
|
|
9
|
+
is_compact_summary, source_file, tool_use_ids: [..]}
|
|
10
|
+
|
|
11
|
+
``tool_use_ids`` carries the ids of ``tool_use`` blocks for assistant
|
|
12
|
+
records and the ``tool_use_id`` of ``tool_result`` blocks for user records
|
|
13
|
+
— the join key back to ATIF ``tool_calls`` / ``observation.source_call_ids``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
#: Stable key order for one edges.jsonl line (contract-fixed shape).
|
|
22
|
+
EDGE_FIELDS: tuple[str, ...] = (
|
|
23
|
+
"uuid",
|
|
24
|
+
"parent_uuid",
|
|
25
|
+
"message_id",
|
|
26
|
+
"type",
|
|
27
|
+
"ts",
|
|
28
|
+
"is_sidechain",
|
|
29
|
+
"is_compact_summary",
|
|
30
|
+
"source_file",
|
|
31
|
+
"tool_use_ids",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _tool_use_ids(record: dict[str, Any]) -> list[str]:
|
|
36
|
+
"""Extract tool_use ids (assistant) or tool_result ids (user) in block order."""
|
|
37
|
+
message = record.get("message")
|
|
38
|
+
if not isinstance(message, dict):
|
|
39
|
+
return []
|
|
40
|
+
content = message.get("content")
|
|
41
|
+
if not isinstance(content, list):
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
record_type = record.get("type")
|
|
45
|
+
ids: list[str] = []
|
|
46
|
+
for block in content:
|
|
47
|
+
if not isinstance(block, dict):
|
|
48
|
+
continue
|
|
49
|
+
if record_type == "assistant" and block.get("type") == "tool_use":
|
|
50
|
+
block_id = block.get("id")
|
|
51
|
+
if isinstance(block_id, str) and block_id:
|
|
52
|
+
ids.append(block_id)
|
|
53
|
+
elif record_type == "user" and block.get("type") == "tool_result":
|
|
54
|
+
block_id = block.get("tool_use_id")
|
|
55
|
+
if isinstance(block_id, str) and block_id:
|
|
56
|
+
ids.append(block_id)
|
|
57
|
+
return ids
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def edge_from_record(record: dict[str, Any], source_file: str) -> dict[str, Any]:
|
|
61
|
+
"""Project one raw record onto the contract's edge shape.
|
|
62
|
+
|
|
63
|
+
``source_file`` is the record's origin path RELATIVE to the session
|
|
64
|
+
JSONL's parent directory (e.g. ``<sid>.jsonl`` or
|
|
65
|
+
``<sid>/subagents/workflows/wf_x/agent-y.jsonl``).
|
|
66
|
+
"""
|
|
67
|
+
message = record.get("message")
|
|
68
|
+
message_id = message.get("id") if isinstance(message, dict) else None
|
|
69
|
+
return {
|
|
70
|
+
"uuid": record.get("uuid"),
|
|
71
|
+
"parent_uuid": record.get("parentUuid"),
|
|
72
|
+
"message_id": message_id if isinstance(message_id, str) else None,
|
|
73
|
+
"type": record.get("type"),
|
|
74
|
+
"ts": record.get("timestamp"),
|
|
75
|
+
"is_sidechain": bool(record.get("isSidechain", False)),
|
|
76
|
+
"is_compact_summary": bool(record.get("isCompactSummary", False)),
|
|
77
|
+
"source_file": source_file,
|
|
78
|
+
"tool_use_ids": _tool_use_ids(record),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_edges(records: list[tuple[dict[str, Any], str]]) -> list[dict[str, Any]]:
|
|
83
|
+
"""Build all edges from ``(record, source_file)`` pairs.
|
|
84
|
+
|
|
85
|
+
Ordered deterministically by ``(ts, uuid)``, with missing values sorting
|
|
86
|
+
first as empty strings.
|
|
87
|
+
"""
|
|
88
|
+
edges = [edge_from_record(record, source_file) for record, source_file in records]
|
|
89
|
+
try:
|
|
90
|
+
edges.sort(key=lambda e: (e["ts"] or "", e["uuid"] or ""))
|
|
91
|
+
except TypeError:
|
|
92
|
+
# A `timestamp` or `uuid` that is not a string (an object, a list, a number beside a
|
|
93
|
+
# string) cannot be compared with the others; the fuzzer found the `TypeError` this
|
|
94
|
+
# raised. Such values sort as missing ones do. Only an order that raised takes this
|
|
95
|
+
# branch, so every input that converted before keeps its exact order.
|
|
96
|
+
edges.sort(key=lambda e: (_sort_text(e["ts"]), _sort_text(e["uuid"])))
|
|
97
|
+
return edges
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _sort_text(value: object) -> str:
|
|
101
|
+
"""A sort key part: the string itself, or ``""`` for a missing or non-string value."""
|
|
102
|
+
return value if isinstance(value, str) else ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def edge_to_jsonl_line(edge: dict[str, Any]) -> str:
|
|
106
|
+
"""Serialize one edge as a compact, key-order-stable JSONL line."""
|
|
107
|
+
ordered = {key: edge.get(key) for key in EDGE_FIELDS}
|
|
108
|
+
return json.dumps(ordered, separators=(",", ":"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def edges_jsonl_lines(records: list[tuple[dict[str, Any], str]]) -> list[str]:
|
|
112
|
+
"""``build_edges`` + serialization: the ready-to-write edges.jsonl body."""
|
|
113
|
+
return [edge_to_jsonl_line(edge) for edge in build_edges(records)]
|