trulens-apps-opencode 2.14.0__tar.gz
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.
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trulens-apps-opencode
|
|
3
|
+
Version: 2.14.0
|
|
4
|
+
Summary: OpenCode hook instrumentation plugin for TruLens.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Snowflake Inc.
|
|
7
|
+
Author-email: ml-observability-wg-dl@snowflake.com
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: trulens-core (>=2.0.0,<3.0.0)
|
|
18
|
+
Project-URL: Homepage, https://trulens.org/
|
|
19
|
+
Project-URL: Repository, https://github.com/truera/trulens
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# trulens-apps-opencode
|
|
23
|
+
|
|
24
|
+
OpenCode plugin configuration and payload mapping for TruLens client-hook
|
|
25
|
+
instrumentation.
|
|
26
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "poetry.core.masonry.api"
|
|
3
|
+
requires = [
|
|
4
|
+
"poetry-core",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
[tool.poetry]
|
|
8
|
+
name = "trulens-apps-opencode"
|
|
9
|
+
version = "2.14.0"
|
|
10
|
+
description = "OpenCode hook instrumentation plugin for TruLens."
|
|
11
|
+
authors = [
|
|
12
|
+
"Snowflake Inc. <ml-observability-wg-dl@snowflake.com>",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
packages = [
|
|
17
|
+
{ include = "trulens" },
|
|
18
|
+
]
|
|
19
|
+
homepage = "https://trulens.org/"
|
|
20
|
+
repository = "https://github.com/truera/trulens"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.dependencies]
|
|
23
|
+
python = "^3.9"
|
|
24
|
+
trulens-core = { version = "^2.0.0" }
|
|
25
|
+
|
|
26
|
+
[tool.poetry.group.dev.dependencies]
|
|
27
|
+
trulens-core = { path = "../../core" }
|
|
28
|
+
|
|
29
|
+
[tool.poetry.plugins."trulens.client_hooks"]
|
|
30
|
+
opencode = "trulens.apps.opencode:client_spec"
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Minimal OpenCode-native plugin specification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
|
|
11
|
+
from trulens.core.otel.client_hooks.clients import ClientSpec
|
|
12
|
+
from trulens.core.otel.client_hooks.clients import FieldAliases
|
|
13
|
+
|
|
14
|
+
_HOOK_EVENTS = (
|
|
15
|
+
"chat.message",
|
|
16
|
+
"tool.execute.before",
|
|
17
|
+
"tool.execute.after",
|
|
18
|
+
"experimental.text.complete",
|
|
19
|
+
"session.idle",
|
|
20
|
+
"session.error",
|
|
21
|
+
"file.edited",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _first(payload: Mapping[str, Any], *keys: str) -> Any:
|
|
26
|
+
for key in keys:
|
|
27
|
+
value = payload.get(key)
|
|
28
|
+
if value is not None:
|
|
29
|
+
return value
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _nested(payload: Mapping[str, Any], *path: str) -> Any:
|
|
34
|
+
current: Any = payload
|
|
35
|
+
for key in path:
|
|
36
|
+
if not isinstance(current, Mapping):
|
|
37
|
+
return None
|
|
38
|
+
current = current.get(key)
|
|
39
|
+
return current
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _prompt_from_parts(parts: Any) -> Any:
|
|
43
|
+
if not isinstance(parts, list):
|
|
44
|
+
return None
|
|
45
|
+
texts = [
|
|
46
|
+
part.get("text")
|
|
47
|
+
for part in parts
|
|
48
|
+
if isinstance(part, Mapping) and part.get("text")
|
|
49
|
+
]
|
|
50
|
+
return "\n".join(texts) or None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_overrides(payload: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
54
|
+
"""Flatten OpenCode camelCase and nested event payloads."""
|
|
55
|
+
|
|
56
|
+
event = payload.get("event")
|
|
57
|
+
properties = event.get("properties") if isinstance(event, Mapping) else {}
|
|
58
|
+
if not isinstance(properties, Mapping):
|
|
59
|
+
properties = {}
|
|
60
|
+
event_name = payload.get("hook_event_name")
|
|
61
|
+
if event_name is None and isinstance(event, Mapping):
|
|
62
|
+
event_name = event.get("type")
|
|
63
|
+
model = _first(payload, "model")
|
|
64
|
+
if isinstance(model, Mapping):
|
|
65
|
+
model = model.get("modelID") or model.get("model_id")
|
|
66
|
+
prompt = _first(payload, "prompt") or _prompt_from_parts(
|
|
67
|
+
payload.get("parts")
|
|
68
|
+
)
|
|
69
|
+
if prompt is None:
|
|
70
|
+
message = payload.get("message")
|
|
71
|
+
if isinstance(message, Mapping):
|
|
72
|
+
prompt = message.get("content") or _prompt_from_parts(
|
|
73
|
+
message.get("parts")
|
|
74
|
+
)
|
|
75
|
+
overrides = {
|
|
76
|
+
"conversation_id": _first(
|
|
77
|
+
payload,
|
|
78
|
+
"session_id",
|
|
79
|
+
"sessionID",
|
|
80
|
+
)
|
|
81
|
+
or properties.get("sessionID")
|
|
82
|
+
or _nested(payload, "event", "properties", "sessionID"),
|
|
83
|
+
"turn_id": _first(payload, "message_id", "messageID"),
|
|
84
|
+
"operation_id": _first(payload, "call_id", "callID", "tool_call_id"),
|
|
85
|
+
"event_name": event_name,
|
|
86
|
+
"tool_name": _first(payload, "tool_name", "tool"),
|
|
87
|
+
"prompt": prompt,
|
|
88
|
+
"response": _first(payload, "response", "text")
|
|
89
|
+
or _nested(payload, "output", "text"),
|
|
90
|
+
"tool_input": _first(payload, "tool_input", "args")
|
|
91
|
+
or _nested(payload, "output", "args"),
|
|
92
|
+
"tool_output": _first(payload, "tool_output")
|
|
93
|
+
or _nested(payload, "output", "output"),
|
|
94
|
+
}
|
|
95
|
+
if model:
|
|
96
|
+
overrides["model"] = model
|
|
97
|
+
return {key: value for key, value in overrides.items() if value is not None}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _detect_version() -> str | None:
|
|
101
|
+
executable = shutil.which("opencode")
|
|
102
|
+
if executable is None:
|
|
103
|
+
return None
|
|
104
|
+
try:
|
|
105
|
+
result = subprocess.run(
|
|
106
|
+
[executable, "--version"],
|
|
107
|
+
capture_output=True,
|
|
108
|
+
check=True,
|
|
109
|
+
text=True,
|
|
110
|
+
timeout=5,
|
|
111
|
+
)
|
|
112
|
+
except (OSError, subprocess.SubprocessError):
|
|
113
|
+
return None
|
|
114
|
+
return result.stdout.strip() or None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _plugin(command: str, client_version: str | None = None) -> str:
|
|
118
|
+
encoded = json.dumps(command)
|
|
119
|
+
encoded_version = json.dumps(client_version)
|
|
120
|
+
return f"""// managed_by: trulens-client-hooks
|
|
121
|
+
const COMMAND = {encoded}
|
|
122
|
+
const VERSION = {encoded_version}
|
|
123
|
+
async function ingest(payload) {{
|
|
124
|
+
try {{
|
|
125
|
+
const proc = Bun.spawn(["sh", "-c", COMMAND], {{
|
|
126
|
+
stdin: "pipe",
|
|
127
|
+
stdout: "ignore",
|
|
128
|
+
stderr: "pipe",
|
|
129
|
+
}})
|
|
130
|
+
proc.stdin.write(JSON.stringify(payload))
|
|
131
|
+
proc.stdin.end()
|
|
132
|
+
await proc.exited
|
|
133
|
+
}} catch (_error) {{
|
|
134
|
+
// Fail open: telemetry must never block OpenCode.
|
|
135
|
+
}}
|
|
136
|
+
}}
|
|
137
|
+
|
|
138
|
+
function textFromParts(parts) {{
|
|
139
|
+
if (!Array.isArray(parts)) {{
|
|
140
|
+
return undefined
|
|
141
|
+
}}
|
|
142
|
+
return parts
|
|
143
|
+
.map((part) => (part && part.text) || "")
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join("\\n") || undefined
|
|
146
|
+
}}
|
|
147
|
+
|
|
148
|
+
export const TruLensClientHooks = async ({{ directory }}) => {{
|
|
149
|
+
let lastSessionId
|
|
150
|
+
const send = async (payload) => {{
|
|
151
|
+
if (payload.session_id) {{
|
|
152
|
+
lastSessionId = payload.session_id
|
|
153
|
+
}}
|
|
154
|
+
await ingest({{ cwd: directory, client_version: VERSION, ...payload }})
|
|
155
|
+
}}
|
|
156
|
+
return {{
|
|
157
|
+
"chat.message": async (input, output) => {{
|
|
158
|
+
await send({{
|
|
159
|
+
session_id: input.sessionID,
|
|
160
|
+
message_id: input.messageID,
|
|
161
|
+
hook_event_name: "chat.message",
|
|
162
|
+
model: input.model && input.model.modelID,
|
|
163
|
+
prompt: textFromParts(output && output.parts),
|
|
164
|
+
}})
|
|
165
|
+
}},
|
|
166
|
+
"tool.execute.before": async (input, output) => {{
|
|
167
|
+
await send({{
|
|
168
|
+
session_id: input.sessionID,
|
|
169
|
+
hook_event_name: "tool.execute.before",
|
|
170
|
+
tool_name: input.tool,
|
|
171
|
+
tool_call_id: input.callID,
|
|
172
|
+
tool_input: output && output.args,
|
|
173
|
+
}})
|
|
174
|
+
}},
|
|
175
|
+
"tool.execute.after": async (input, output) => {{
|
|
176
|
+
await send({{
|
|
177
|
+
session_id: input.sessionID,
|
|
178
|
+
hook_event_name: "tool.execute.after",
|
|
179
|
+
tool_name: input.tool,
|
|
180
|
+
tool_call_id: input.callID,
|
|
181
|
+
tool_input: input.args,
|
|
182
|
+
tool_output: output && output.output,
|
|
183
|
+
}})
|
|
184
|
+
}},
|
|
185
|
+
"experimental.text.complete": async (input, output) => {{
|
|
186
|
+
await send({{
|
|
187
|
+
session_id: input.sessionID,
|
|
188
|
+
message_id: input.messageID,
|
|
189
|
+
response_message_id: input.messageID,
|
|
190
|
+
hook_event_name: "experimental.text.complete",
|
|
191
|
+
text: output && output.text,
|
|
192
|
+
}})
|
|
193
|
+
}},
|
|
194
|
+
event: async ({{ event }}) => {{
|
|
195
|
+
const type = event && event.type
|
|
196
|
+
const properties = (event && event.properties) || {{}}
|
|
197
|
+
const sessionId =
|
|
198
|
+
properties.sessionID || event.sessionID || lastSessionId
|
|
199
|
+
if (type === "session.idle" || type === "session.error") {{
|
|
200
|
+
await send({{
|
|
201
|
+
session_id: sessionId,
|
|
202
|
+
hook_event_name: type,
|
|
203
|
+
status: type === "session.error" ? "error" : "completed",
|
|
204
|
+
error:
|
|
205
|
+
type === "session.error"
|
|
206
|
+
? properties.error || event.error
|
|
207
|
+
: undefined,
|
|
208
|
+
}})
|
|
209
|
+
}}
|
|
210
|
+
if (type === "file.edited") {{
|
|
211
|
+
await send({{
|
|
212
|
+
session_id: sessionId,
|
|
213
|
+
hook_event_name: "file.edited",
|
|
214
|
+
file_path: properties.file || event.path,
|
|
215
|
+
}})
|
|
216
|
+
}}
|
|
217
|
+
}},
|
|
218
|
+
}}
|
|
219
|
+
}}
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
client_spec = ClientSpec(
|
|
224
|
+
name="opencode",
|
|
225
|
+
aliases=("open-code",),
|
|
226
|
+
user_config_path=Path("~/.config/opencode/plugins/trulens-client-hooks.js"),
|
|
227
|
+
project_config_path=Path(".opencode/plugins/trulens-client-hooks.js"),
|
|
228
|
+
hook_events=_HOOK_EVENTS,
|
|
229
|
+
field_aliases=FieldAliases(
|
|
230
|
+
conversation=("session_id", "sessionID"),
|
|
231
|
+
turn=("message_id", "messageID"),
|
|
232
|
+
operation=("call_id", "callID", "tool_call_id"),
|
|
233
|
+
response=("text", "response", "output"),
|
|
234
|
+
),
|
|
235
|
+
plugin_builder=_plugin,
|
|
236
|
+
version_detector=_detect_version,
|
|
237
|
+
extract_overrides=extract_overrides,
|
|
238
|
+
)
|