agent-trajectory 0.2.3__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.
trajectory/_client.py ADDED
@@ -0,0 +1,236 @@
1
+ """Subprocess transport for the bundled trajectory runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from collections.abc import Iterable, Mapping
11
+ from functools import lru_cache
12
+ from pathlib import Path
13
+ from typing import cast
14
+
15
+ from ._errors import NodeUnavailableError, NormalizationError, TrajectoryRuntimeError
16
+ from ._types import (
17
+ AnyTrajectorySource,
18
+ ListTrajectoriesResult,
19
+ NormalizationBounds,
20
+ NormalizationFilters,
21
+ NormalizationErrorCode,
22
+ NormalizeInput,
23
+ NormalizeRequest,
24
+ NormalizeResult,
25
+ SourceContext,
26
+ TrajectorySource,
27
+ )
28
+
29
+ _PROTOCOL_VERSION = 1
30
+ _MINIMUM_NODE_MAJOR = 20
31
+ _CLI_PATH = Path(__file__).parent / "_vendor" / "trajectory-cli.mjs"
32
+
33
+
34
+ def normalize_transcript(
35
+ *,
36
+ source: TrajectorySource,
37
+ transcript: str,
38
+ bounds: NormalizationBounds | None = None,
39
+ filters: NormalizationFilters | None = None,
40
+ source_context: SourceContext | None = None,
41
+ ) -> NormalizeResult:
42
+ """Normalize one native transcript or an explicitly partial fragment.
43
+
44
+ ``source_context`` mirrors the TypeScript ``sourceContext`` input. Set
45
+ ``{"partial": True}`` for an offset-zero fragment; a non-zero
46
+ ``baseByteOffset`` also implies partial mode.
47
+ """
48
+
49
+ request: NormalizeInput = {"source": source, "transcript": transcript}
50
+ if bounds is not None:
51
+ request["bounds"] = bounds
52
+ if filters is not None:
53
+ request["filters"] = filters
54
+ if source_context is not None:
55
+ request["sourceContext"] = source_context
56
+ return normalize_many([request])[0]
57
+
58
+
59
+ def normalize_checkpoint(
60
+ *,
61
+ thread_id: str,
62
+ path: str | Path | None = None,
63
+ bounds: NormalizationBounds | None = None,
64
+ filters: NormalizationFilters | None = None,
65
+ python_executable: str | None = None,
66
+ ) -> NormalizeResult:
67
+ """Normalize one Deep Agents thread from its LangGraph SQLite store.
68
+
69
+ ``path`` defaults to the Deep Agents CLI store, ``~/.deepagents/sessions.db``.
70
+ """
71
+
72
+ checkpoint: dict[str, str] = {
73
+ "threadId": thread_id,
74
+ "pythonExecutable": python_executable or sys.executable,
75
+ }
76
+ if path is not None:
77
+ checkpoint["path"] = str(path)
78
+ request: NormalizeRequest = {
79
+ "source": "deepagents",
80
+ "checkpoint": checkpoint,
81
+ }
82
+ if bounds is not None:
83
+ request["bounds"] = bounds
84
+ if filters is not None:
85
+ request["filters"] = filters
86
+ return normalize_many([request])[0]
87
+
88
+
89
+ def list_trajectories(
90
+ *,
91
+ source: AnyTrajectorySource,
92
+ root: str | Path | None = None,
93
+ cursor: str | None = None,
94
+ limit: int | None = None,
95
+ ) -> ListTrajectoriesResult:
96
+ """List the trajectories in a source's local store, newest first.
97
+
98
+ Returns ``{"items": [...], "nextCursor": ...}``; ``nextCursor`` is present
99
+ exactly when more items remain. ``root`` overrides the source's standard
100
+ store location.
101
+ """
102
+
103
+ query: dict[str, object] = {"source": source}
104
+ if root is not None:
105
+ query["root"] = str(root)
106
+ if cursor is not None:
107
+ query["cursor"] = cursor
108
+ if limit is not None:
109
+ query["limit"] = limit
110
+ result = normalize_many([cast(NormalizeRequest, {"list": query})])[0]
111
+ return cast(ListTrajectoriesResult, result)
112
+
113
+
114
+ def normalize_many(inputs: Iterable[NormalizeRequest]) -> list[NormalizeResult]:
115
+ """Normalize multiple transcript or checkpoint requests in one Node.js subprocess.
116
+
117
+ Results preserve input order. If a transcript fails, ``NormalizationError``
118
+ identifies its zero-based ``input_index``.
119
+ """
120
+
121
+ raw_requests = list(inputs)
122
+ if not raw_requests:
123
+ return []
124
+ requests: list[dict[str, object]] = []
125
+ for index, request in enumerate(raw_requests):
126
+ if not isinstance(request, Mapping):
127
+ raise TypeError(f"Input {index} must be a mapping.")
128
+ normalized_request = dict(request)
129
+ if normalized_request.get("source") == "deepagents":
130
+ checkpoint = normalized_request.get("checkpoint")
131
+ if isinstance(checkpoint, Mapping):
132
+ normalized_checkpoint = dict(checkpoint)
133
+ normalized_checkpoint.setdefault("pythonExecutable", sys.executable)
134
+ normalized_request["checkpoint"] = normalized_checkpoint
135
+ requests.append(normalized_request)
136
+
137
+ payload = json.dumps(
138
+ {"version": _PROTOCOL_VERSION, "requests": requests},
139
+ ensure_ascii=False,
140
+ separators=(",", ":"),
141
+ )
142
+ completed = _run_bridge(payload)
143
+ try:
144
+ response = json.loads(completed.stdout)
145
+ except json.JSONDecodeError as error:
146
+ raise TrajectoryRuntimeError(
147
+ "The trajectory runtime returned invalid JSON."
148
+ ) from error
149
+
150
+ if not isinstance(response, dict) or response.get("version") != _PROTOCOL_VERSION:
151
+ raise TrajectoryRuntimeError(
152
+ "The trajectory runtime protocol version did not match."
153
+ )
154
+ results = response.get("results")
155
+ if not isinstance(results, list) or len(results) != len(requests):
156
+ raise TrajectoryRuntimeError(
157
+ "The trajectory runtime returned an invalid result count."
158
+ )
159
+
160
+ normalized: list[NormalizeResult] = []
161
+ for index, item in enumerate(results):
162
+ if not isinstance(item, dict):
163
+ raise TrajectoryRuntimeError(
164
+ f"The trajectory runtime returned an invalid result at index {index}."
165
+ )
166
+ if item.get("ok") is True and isinstance(item.get("result"), dict):
167
+ normalized.append(cast(NormalizeResult, item["result"]))
168
+ continue
169
+ normalization_error = item.get("error")
170
+ if item.get("ok") is False and isinstance(normalization_error, dict):
171
+ code = normalization_error.get("code")
172
+ message = normalization_error.get("message")
173
+ if isinstance(code, str) and isinstance(message, str):
174
+ if code == "internal_error":
175
+ raise TrajectoryRuntimeError(
176
+ f"Trajectory runtime error for input {index}: {message}"
177
+ )
178
+ raise NormalizationError(
179
+ cast(NormalizationErrorCode, code), message, input_index=index
180
+ )
181
+ raise TrajectoryRuntimeError(
182
+ f"The trajectory runtime returned an invalid result at index {index}."
183
+ )
184
+ return normalized
185
+
186
+
187
+ def _run_bridge(payload: str) -> subprocess.CompletedProcess[str]:
188
+ node = _node_executable()
189
+ if not _CLI_PATH.is_file():
190
+ raise TrajectoryRuntimeError(
191
+ f"The bundled trajectory runtime is missing at {_CLI_PATH}."
192
+ )
193
+ try:
194
+ return subprocess.run(
195
+ [node, str(_CLI_PATH)],
196
+ input=payload,
197
+ text=True,
198
+ encoding="utf-8",
199
+ capture_output=True,
200
+ check=True,
201
+ )
202
+ except subprocess.CalledProcessError as error:
203
+ detail = error.stderr.strip() or f"exit status {error.returncode}"
204
+ raise TrajectoryRuntimeError(
205
+ f"The trajectory runtime failed: {detail}"
206
+ ) from error
207
+ except OSError as error:
208
+ raise TrajectoryRuntimeError(
209
+ f"Could not execute the trajectory runtime: {error}"
210
+ ) from error
211
+
212
+
213
+ @lru_cache(maxsize=1)
214
+ def _node_executable() -> str:
215
+ node = shutil.which("node")
216
+ if node is None:
217
+ raise NodeUnavailableError("trajectory requires Node.js 20 or newer.")
218
+ try:
219
+ completed = subprocess.run(
220
+ [node, "--version"],
221
+ text=True,
222
+ encoding="utf-8",
223
+ capture_output=True,
224
+ check=True,
225
+ )
226
+ except (OSError, subprocess.CalledProcessError) as error:
227
+ raise NodeUnavailableError(
228
+ "trajectory could not determine the installed Node.js version."
229
+ ) from error
230
+ match = re.fullmatch(r"v(\d+)(?:\.\d+){2}\s*", completed.stdout)
231
+ if match is None or int(match.group(1)) < _MINIMUM_NODE_MAJOR:
232
+ version = completed.stdout.strip() or "unknown"
233
+ raise NodeUnavailableError(
234
+ f"trajectory requires Node.js 20 or newer; found {version}."
235
+ )
236
+ return node
trajectory/_errors.py ADDED
@@ -0,0 +1,22 @@
1
+ """Exceptions raised by the Python wrapper."""
2
+
3
+ from ._types import NormalizationErrorCode
4
+
5
+
6
+ class TrajectoryRuntimeError(RuntimeError):
7
+ """The bundled trajectory runtime could not be executed reliably."""
8
+
9
+
10
+ class NodeUnavailableError(TrajectoryRuntimeError):
11
+ """Node.js 20 or newer could not be found."""
12
+
13
+
14
+ class NormalizationError(ValueError):
15
+ """A transcript could not be normalized."""
16
+
17
+ def __init__(
18
+ self, code: NormalizationErrorCode, message: str, *, input_index: int = 0
19
+ ) -> None:
20
+ super().__init__(message)
21
+ self.code = code
22
+ self.input_index = input_index
trajectory/_types.py ADDED
@@ -0,0 +1,203 @@
1
+ """Public typing contract for trajectory inputs and outputs."""
2
+
3
+ from typing import Literal, TypedDict, Union
4
+
5
+ TrajectorySource = Literal[
6
+ "claude-code", "codex", "copilot-cli", "cursor", "droid", "gemini-cli", "hermes", "letta-code", "omp", "openclaw", "opencode", "openhands", "pi"
7
+ ]
8
+ CheckpointTrajectorySource = Literal["deepagents"]
9
+ AnyTrajectorySource = Literal[
10
+ "claude-code", "codex", "copilot-cli", "cursor", "droid", "gemini-cli", "hermes", "letta-code", "omp", "openclaw", "opencode", "openhands", "pi", "deepagents"
11
+ ]
12
+ ToolResultTruncationStrategy = Literal["head", "head-tail"]
13
+ ToolResultPolicy = Literal["include", "omit"]
14
+ DiagnosticCode = Literal[
15
+ "invalid_json_line",
16
+ "non_object_json_line",
17
+ "injected_context_dropped",
18
+ "noise_record_dropped",
19
+ "sidechain_record_dropped",
20
+ "tool_call_id_synthesized",
21
+ "duplicate_tool_call_id",
22
+ "orphan_tool_result",
23
+ "duplicate_tool_result",
24
+ "unknown_tool_name",
25
+ "tool_arguments_reshaped",
26
+ "tool_arguments_truncated",
27
+ "tool_result_truncated",
28
+ "timestamps_synthesized",
29
+ "timestamps_interpolated",
30
+ ]
31
+ NormalizationErrorCode = Literal[
32
+ "invalid_input",
33
+ "unknown_source",
34
+ "python_unavailable",
35
+ "python_dependency_missing",
36
+ "checkpoint_database_not_found",
37
+ "checkpoint_database_unreadable",
38
+ "checkpoint_read_failed",
39
+ "checkpoint_not_found",
40
+ "checkpoint_messages_missing",
41
+ "invalid_checkpoint_state",
42
+ "listing_unavailable",
43
+ "missing_user_records",
44
+ "missing_assistant_records",
45
+ "invalid_normalized_transcript",
46
+ "source_group_required",
47
+ "source_group_conflict",
48
+ ]
49
+
50
+
51
+ class _TrajectoryListingOptional(TypedDict, total=False):
52
+ updatedAt: str
53
+ title: str
54
+ sizeBytes: int
55
+
56
+
57
+ class TrajectoryListing(_TrajectoryListingOptional):
58
+ id: str
59
+ path: str
60
+
61
+
62
+ class _ListTrajectoriesResultOptional(TypedDict, total=False):
63
+ nextCursor: str
64
+
65
+
66
+ class ListTrajectoriesResult(_ListTrajectoriesResultOptional):
67
+ items: list[TrajectoryListing]
68
+
69
+
70
+ class ToolArgumentBounds(TypedDict, total=False):
71
+ maxCharacters: int | None
72
+
73
+
74
+ class ToolResultBounds(TypedDict, total=False):
75
+ maxCharacters: int | None
76
+ strategy: ToolResultTruncationStrategy
77
+
78
+
79
+ class NormalizationBounds(TypedDict, total=False):
80
+ toolArguments: ToolArgumentBounds
81
+ toolResults: ToolResultBounds
82
+
83
+
84
+ class NormalizationFilters(TypedDict, total=False):
85
+ toolResults: ToolResultPolicy
86
+
87
+
88
+ class SourceContext(TypedDict, total=False):
89
+ groupId: str
90
+ baseByteOffset: int
91
+ partial: bool
92
+
93
+
94
+ class _NormalizeInputOptional(TypedDict, total=False):
95
+ bounds: NormalizationBounds
96
+ filters: NormalizationFilters
97
+ sourceContext: SourceContext
98
+
99
+
100
+ class NormalizeInput(_NormalizeInputOptional):
101
+ source: TrajectorySource
102
+ transcript: str
103
+
104
+
105
+ class _DeepAgentsCheckpointLocationOptional(TypedDict, total=False):
106
+ path: str
107
+ pythonExecutable: str
108
+
109
+
110
+ class DeepAgentsCheckpointLocation(_DeepAgentsCheckpointLocationOptional):
111
+ threadId: str
112
+
113
+
114
+ class _DeepAgentsCheckpointInputOptional(TypedDict, total=False):
115
+ bounds: NormalizationBounds
116
+ filters: NormalizationFilters
117
+
118
+
119
+ class DeepAgentsCheckpointInput(_DeepAgentsCheckpointInputOptional):
120
+ source: Literal["deepagents"]
121
+ checkpoint: DeepAgentsCheckpointLocation
122
+
123
+
124
+ NormalizeRequest = Union[NormalizeInput, DeepAgentsCheckpointInput]
125
+
126
+
127
+ class _DiagnosticOptional(TypedDict, total=False):
128
+ inputLine: int
129
+ recordIndex: int
130
+ count: int
131
+
132
+
133
+ class Diagnostic(_DiagnosticOptional):
134
+ code: DiagnosticCode
135
+ message: str
136
+
137
+
138
+ class _MetaOptional(TypedDict, total=False):
139
+ cwd: str
140
+ git_branch: str
141
+ model: str
142
+
143
+
144
+ class MetaRecord(_MetaOptional):
145
+ role: Literal["meta"]
146
+ source: str
147
+
148
+
149
+ class UserRecord(TypedDict):
150
+ role: Literal["user"]
151
+ content: str
152
+ timestamp: str
153
+
154
+
155
+ class ReasoningRecord(TypedDict):
156
+ role: Literal["reasoning"]
157
+ content: str
158
+ timestamp: str
159
+
160
+
161
+ class AssistantMessageRecord(TypedDict):
162
+ role: Literal["assistant"]
163
+ content: str
164
+ timestamp: str
165
+
166
+
167
+ class ToolCall(TypedDict):
168
+ id: str
169
+ name: str
170
+ args: str
171
+
172
+
173
+ class AssistantToolCallRecord(TypedDict):
174
+ role: Literal["assistant"]
175
+ content: None
176
+ tool_calls: list[ToolCall]
177
+ timestamp: str
178
+
179
+
180
+ class _ToolResultOptional(TypedDict, total=False):
181
+ ok: bool
182
+
183
+
184
+ class ToolResultRecord(_ToolResultOptional):
185
+ role: Literal["tool"]
186
+ tool_call_id: str
187
+ content: str
188
+ timestamp: str
189
+
190
+
191
+ NormalizedRecord = Union[
192
+ MetaRecord,
193
+ UserRecord,
194
+ ReasoningRecord,
195
+ AssistantMessageRecord,
196
+ AssistantToolCallRecord,
197
+ ToolResultRecord,
198
+ ]
199
+
200
+
201
+ class NormalizeResult(TypedDict):
202
+ records: list[NormalizedRecord]
203
+ diagnostics: list[Diagnostic]