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.
- agent_trajectory-0.2.3.dist-info/METADATA +211 -0
- agent_trajectory-0.2.3.dist-info/RECORD +12 -0
- agent_trajectory-0.2.3.dist-info/WHEEL +5 -0
- agent_trajectory-0.2.3.dist-info/licenses/LICENSE +190 -0
- agent_trajectory-0.2.3.dist-info/top_level.txt +1 -0
- trajectory/__init__.py +79 -0
- trajectory/_client.py +236 -0
- trajectory/_errors.py +22 -0
- trajectory/_types.py +203 -0
- trajectory/_vendor/deepagents_checkpoint.py +362 -0
- trajectory/_vendor/trajectory-cli.mjs +3505 -0
- trajectory/py.typed +1 -0
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]
|