@softprobe/opencode-plugin 0.1.0
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.
- package/README.md +70 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +70 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +235 -0
- package/dist/softprobe.d.ts +119 -0
- package/dist/softprobe.js +707 -0
- package/dist/tool-kind.d.ts +3 -0
- package/dist/tool-kind.js +27 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Softprobe OpenCode plugin
|
|
2
|
+
|
|
3
|
+
OpenCode plugin that maps coding-agent sessions to Softprobe OTLP observations
|
|
4
|
+
(`agent` → `generation` → `tool`) using [`@softprobe/tracing`](../tracing).
|
|
5
|
+
|
|
6
|
+
Full message and tool payloads are always captured so sessions can be evaluated
|
|
7
|
+
and used to improve agents.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Enable OpenTelemetry and add the plugin in `opencode.json` / `opencode.jsonc`:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"experimental": {
|
|
16
|
+
"openTelemetry": true
|
|
17
|
+
},
|
|
18
|
+
"plugin": ["@softprobe/opencode-plugin@latest"]
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Restart OpenCode after changing the config.
|
|
23
|
+
|
|
24
|
+
## Credentials
|
|
25
|
+
|
|
26
|
+
Create `opencode-softprobe.json` in the OpenCode / spcode global config directory:
|
|
27
|
+
|
|
28
|
+
- spcode: `$XDG_CONFIG_HOME/spcode` (or `~/.config/spcode`)
|
|
29
|
+
- OpenCode: `$XDG_CONFIG_HOME/opencode` (or `~/.config/opencode`)
|
|
30
|
+
- Override directory with `OPENCODE_CONFIG_DIR`
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"publicKey": "<softprobe-bearer-token>",
|
|
35
|
+
"baseUrl": "https://thelake.softprobe.ai",
|
|
36
|
+
"otlpEndpoint": "https://thelake.softprobe.ai/v1/traces",
|
|
37
|
+
"environment": "production",
|
|
38
|
+
"userId": "your-user-id"
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`publicKey` and `baseUrl` are required. `otlpEndpoint` defaults to
|
|
43
|
+
`{baseUrl}/v1/traces`. Credential parsing and validation use shared helpers from
|
|
44
|
+
`@softprobe/tracing` (`resolveSoftprobeConfigFromEnv`, etc.); this package only
|
|
45
|
+
adds the OpenCode config file path and soft-disable behavior when credentials are
|
|
46
|
+
missing. When both `spcode` and `opencode` credential files exist, **spcode wins**.
|
|
47
|
+
|
|
48
|
+
Or set environment variables (env wins when both key and base URL are set):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
export SOFTPROBE_PUBLIC_KEY="..."
|
|
52
|
+
export SOFTPROBE_BASE_URL="https://thelake.softprobe.ai"
|
|
53
|
+
export SOFTPROBE_OTLP_ENDPOINT="https://thelake.softprobe.ai/v1/traces"
|
|
54
|
+
export SOFTPROBE_ENVIRONMENT="production"
|
|
55
|
+
export SOFTPROBE_USER_ID="your-user-id"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Softprobe **spcode** product builds auto-inject this plugin and default thelake
|
|
59
|
+
credentials when `SPCODE_MODE` is on (see softprobe-code
|
|
60
|
+
`softprobe-llm-defaults.ts`).
|
|
61
|
+
|
|
62
|
+
## What is traced
|
|
63
|
+
|
|
64
|
+
- User turns (`opencode.turn` / `agent`) with prompt text
|
|
65
|
+
- Model generations with completions, usage, and cost
|
|
66
|
+
- Tool executions with arguments and results (`gen_ai.tool.*` / `sp.tool.*`)
|
|
67
|
+
- Retries, reasoning, compaction events
|
|
68
|
+
- Failed steps and session errors / aborts
|
|
69
|
+
|
|
70
|
+
See [Coding agents](../../docs/integrations/coding-agents.md).
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MissingSoftprobeCredentialsError, type ResolvedSoftprobeConfig } from "@softprobe/tracing";
|
|
2
|
+
export type SoftprobePluginCredentials = ResolvedSoftprobeConfig;
|
|
3
|
+
export { MissingSoftprobeCredentialsError };
|
|
4
|
+
/**
|
|
5
|
+
* OpenCode / spcode global config directory (XDG Base Directory).
|
|
6
|
+
* Matches OpenCode: `$OPENCODE_CONFIG_DIR`, else `$XDG_CONFIG_HOME/<app>`,
|
|
7
|
+
* else `~/.config/<app>`. Softprobe spcode uses `spcode`; upstream OpenCode
|
|
8
|
+
* uses `opencode`.
|
|
9
|
+
* @see https://opencode.ai/docs/config/
|
|
10
|
+
*/
|
|
11
|
+
export declare function defaultConfigDir(app?: string): string;
|
|
12
|
+
export declare function defaultConfigPath(): string;
|
|
13
|
+
/** Credential file candidates: env dir, then spcode, then opencode. */
|
|
14
|
+
export declare function candidateConfigPaths(): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Resolve Softprobe credentials for the OpenCode plugin.
|
|
17
|
+
* Env vars win when both `SOFTPROBE_PUBLIC_KEY` and `SOFTPROBE_BASE_URL` are set.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadSoftprobeCredentials(configPath?: string): Promise<SoftprobePluginCredentials>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { MissingSoftprobeCredentialsError, resolveSoftprobeConfigFromEnv, resolveSoftprobeConfigFromObject, } from "@softprobe/tracing";
|
|
5
|
+
export { MissingSoftprobeCredentialsError };
|
|
6
|
+
/**
|
|
7
|
+
* OpenCode / spcode global config directory (XDG Base Directory).
|
|
8
|
+
* Matches OpenCode: `$OPENCODE_CONFIG_DIR`, else `$XDG_CONFIG_HOME/<app>`,
|
|
9
|
+
* else `~/.config/<app>`. Softprobe spcode uses `spcode`; upstream OpenCode
|
|
10
|
+
* uses `opencode`.
|
|
11
|
+
* @see https://opencode.ai/docs/config/
|
|
12
|
+
*/
|
|
13
|
+
export function defaultConfigDir(app = "opencode") {
|
|
14
|
+
const custom = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
15
|
+
if (custom)
|
|
16
|
+
return custom;
|
|
17
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
18
|
+
const configHome = xdg || join(homedir(), ".config");
|
|
19
|
+
return join(configHome, app);
|
|
20
|
+
}
|
|
21
|
+
export function defaultConfigPath() {
|
|
22
|
+
return join(defaultConfigDir("opencode"), "opencode-softprobe.json");
|
|
23
|
+
}
|
|
24
|
+
/** Credential file candidates: env dir, then spcode, then opencode. */
|
|
25
|
+
export function candidateConfigPaths() {
|
|
26
|
+
const custom = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
27
|
+
if (custom)
|
|
28
|
+
return [join(custom, "opencode-softprobe.json")];
|
|
29
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
30
|
+
const configHome = xdg || join(homedir(), ".config");
|
|
31
|
+
return [
|
|
32
|
+
join(configHome, "spcode", "opencode-softprobe.json"),
|
|
33
|
+
join(configHome, "opencode", "opencode-softprobe.json"),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
async function fileExists(path) {
|
|
37
|
+
try {
|
|
38
|
+
await access(path);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve Softprobe credentials for the OpenCode plugin.
|
|
47
|
+
* Env vars win when both `SOFTPROBE_PUBLIC_KEY` and `SOFTPROBE_BASE_URL` are set.
|
|
48
|
+
*/
|
|
49
|
+
export async function loadSoftprobeCredentials(configPath) {
|
|
50
|
+
const envCreds = resolveSoftprobeConfigFromEnv();
|
|
51
|
+
if (envCreds)
|
|
52
|
+
return envCreds;
|
|
53
|
+
const paths = configPath ? [configPath] : candidateConfigPaths();
|
|
54
|
+
const tried = [];
|
|
55
|
+
for (const path of paths) {
|
|
56
|
+
tried.push(path);
|
|
57
|
+
if (!(await fileExists(path)))
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const text = await readFile(path, "utf8");
|
|
61
|
+
return resolveSoftprobeConfigFromObject(JSON.parse(text));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof MissingSoftprobeCredentialsError)
|
|
65
|
+
throw error;
|
|
66
|
+
throw new MissingSoftprobeCredentialsError(`Invalid Softprobe credentials in ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new MissingSoftprobeCredentialsError(`Missing Softprobe credentials (set SOFTPROBE_PUBLIC_KEY + SOFTPROBE_BASE_URL or create one of: ${tried.join(", ")})`);
|
|
70
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Hooks, Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { SoftprobeSessionTracer } from "./softprobe.js";
|
|
3
|
+
/** Build OpenCode hooks from an existing tracer (used by the plugin and tests). */
|
|
4
|
+
export declare function createHooksFromTracer(tracer: SoftprobeSessionTracer): Hooks;
|
|
5
|
+
export declare const SoftprobePlugin: Plugin;
|
|
6
|
+
/**
|
|
7
|
+
* OpenCode V1 plugin shape (`{ id, server }`).
|
|
8
|
+
* Required so file:// installs do not fall through to legacy export scanning,
|
|
9
|
+
* which would invoke every exported function/class (including Error subclasses).
|
|
10
|
+
*/
|
|
11
|
+
declare const _default: {
|
|
12
|
+
id: string;
|
|
13
|
+
server: Plugin;
|
|
14
|
+
};
|
|
15
|
+
export default _default;
|
|
16
|
+
export { SoftprobeSessionTracer, createSoftprobeSessionTracer, } from "./softprobe.js";
|
|
17
|
+
export { loadSoftprobeCredentials, MissingSoftprobeCredentialsError, defaultConfigDir, defaultConfigPath, type SoftprobePluginCredentials, } from "./config.js";
|
|
18
|
+
export { inferToolKind } from "./tool-kind.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { loadSoftprobeCredentials, MissingSoftprobeCredentialsError, } from "./config.js";
|
|
2
|
+
import { createSoftprobeSessionTracer, } from "./softprobe.js";
|
|
3
|
+
function log(level, message) {
|
|
4
|
+
const line = `[softprobe-opencode] ${message}`;
|
|
5
|
+
if (level === "error")
|
|
6
|
+
console.error(line);
|
|
7
|
+
else if (level === "warn")
|
|
8
|
+
console.warn(line);
|
|
9
|
+
else
|
|
10
|
+
console.info(line);
|
|
11
|
+
}
|
|
12
|
+
function formatHookError(error) {
|
|
13
|
+
if (error instanceof Error)
|
|
14
|
+
return error.stack ?? error.message;
|
|
15
|
+
if (Array.isArray(error)) {
|
|
16
|
+
return error.map((item) => formatHookError(item)).join("; ") || "[]";
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const json = JSON.stringify(error);
|
|
20
|
+
if (json && json !== "{}" && json !== "[{}]")
|
|
21
|
+
return json;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// fall through
|
|
25
|
+
}
|
|
26
|
+
return String(error);
|
|
27
|
+
}
|
|
28
|
+
function createShutdownOnce(tracer) {
|
|
29
|
+
let shutdownPromise;
|
|
30
|
+
return () => {
|
|
31
|
+
if (!shutdownPromise) {
|
|
32
|
+
shutdownPromise = tracer.shutdown().catch((error) => {
|
|
33
|
+
log("error", `shutdown failed: ${formatHookError(error)}`);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return shutdownPromise;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function handleEvent(tracer, event, shutdown) {
|
|
40
|
+
if (event.type === "session.idle") {
|
|
41
|
+
log("info", "Flushing spans");
|
|
42
|
+
tracer.finalizeSessionTracing();
|
|
43
|
+
await tracer.forceFlush();
|
|
44
|
+
}
|
|
45
|
+
if (event.type === "server.instance.disposed") {
|
|
46
|
+
tracer.finalizeSessionTracing();
|
|
47
|
+
if (shutdown)
|
|
48
|
+
await shutdown();
|
|
49
|
+
}
|
|
50
|
+
if (event.type === "session.error" && "properties" in event) {
|
|
51
|
+
const props = event.properties;
|
|
52
|
+
if (props.sessionID) {
|
|
53
|
+
tracer.traceSessionError({
|
|
54
|
+
sessionID: props.sessionID,
|
|
55
|
+
error: props.error,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (event.type === "message.part.updated" && "properties" in event) {
|
|
60
|
+
const part = event.properties.part;
|
|
61
|
+
tracer.rememberAssistantPart(part);
|
|
62
|
+
tracer.traceReasoningPart(part);
|
|
63
|
+
tracer.traceToolPart(part);
|
|
64
|
+
}
|
|
65
|
+
if (event.type === "session.next.step.started") {
|
|
66
|
+
tracer.startActiveGenerationStep({
|
|
67
|
+
sessionID: event.properties.sessionID,
|
|
68
|
+
agent: event.properties.agent,
|
|
69
|
+
model: event.properties.model,
|
|
70
|
+
started: event.properties.timestamp,
|
|
71
|
+
snapshot: event.properties.snapshot,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (event.type === "session.next.step.failed") {
|
|
75
|
+
tracer.traceFailedGenerationStep({
|
|
76
|
+
id: event.id,
|
|
77
|
+
sessionID: event.properties.sessionID,
|
|
78
|
+
completed: event.properties.timestamp,
|
|
79
|
+
error: event.properties.error,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (event.type === "session.next.retried") {
|
|
83
|
+
tracer.traceEvent({
|
|
84
|
+
id: event.id,
|
|
85
|
+
sessionID: event.properties.sessionID,
|
|
86
|
+
name: "opencode.generation.retry",
|
|
87
|
+
timestamp: event.properties.timestamp,
|
|
88
|
+
output: event.properties.error,
|
|
89
|
+
metadata: { attempt: event.properties.attempt },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (event.type === "session.next.reasoning.ended") {
|
|
93
|
+
tracer.traceReasoning({
|
|
94
|
+
reasoningID: event.properties.reasoningID,
|
|
95
|
+
sessionID: event.properties.sessionID,
|
|
96
|
+
timestamp: event.properties.timestamp,
|
|
97
|
+
text: event.properties.text,
|
|
98
|
+
messageID: event.properties.assistantMessageID,
|
|
99
|
+
source: "session.next.reasoning.ended",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (event.type === "session.next.compaction.ended") {
|
|
103
|
+
tracer.traceEvent({
|
|
104
|
+
id: event.id,
|
|
105
|
+
sessionID: event.properties.sessionID,
|
|
106
|
+
name: "opencode.generation.compaction",
|
|
107
|
+
timestamp: event.properties.timestamp,
|
|
108
|
+
output: { text: event.properties.text },
|
|
109
|
+
metadata: {
|
|
110
|
+
include: event.properties.include ?? null,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (event.type === "message.updated" && "properties" in event) {
|
|
115
|
+
const message = event.properties.info;
|
|
116
|
+
if (message.role !== "assistant" || !message.time.completed) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
tracer.traceGeneration({
|
|
120
|
+
sessionID: message.sessionID,
|
|
121
|
+
messageID: message.id,
|
|
122
|
+
parentID: message.parentID,
|
|
123
|
+
modelID: message.modelID,
|
|
124
|
+
providerID: message.providerID,
|
|
125
|
+
agent: message.mode,
|
|
126
|
+
mode: message.mode,
|
|
127
|
+
created: message.time.created,
|
|
128
|
+
completed: message.time.completed,
|
|
129
|
+
finish: message.finish,
|
|
130
|
+
cost: message.cost,
|
|
131
|
+
tokens: message.tokens,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function safeRun(hookName, fn) {
|
|
136
|
+
return Promise.resolve()
|
|
137
|
+
.then(fn)
|
|
138
|
+
.catch((error) => {
|
|
139
|
+
log("error", `hook "${hookName}" failed: ${formatHookError(error)}`);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Build OpenCode hooks from an existing tracer (used by the plugin and tests). */
|
|
143
|
+
export function createHooksFromTracer(tracer) {
|
|
144
|
+
const shutdownOnce = createShutdownOnce(tracer);
|
|
145
|
+
return {
|
|
146
|
+
dispose: () => safeRun("dispose", async () => {
|
|
147
|
+
tracer.finalizeSessionTracing();
|
|
148
|
+
await shutdownOnce();
|
|
149
|
+
}),
|
|
150
|
+
config: (config) => safeRun("config", () => {
|
|
151
|
+
if (!config.experimental?.openTelemetry) {
|
|
152
|
+
log("warn", "[Tracing disabled] Enable `experimental.openTelemetry` in opencode.json to use the Softprobe plugin");
|
|
153
|
+
}
|
|
154
|
+
}),
|
|
155
|
+
event: ({ event }) => safeRun("event", async () => {
|
|
156
|
+
try {
|
|
157
|
+
await handleEvent(tracer, event, shutdownOnce);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
// forceFlush often rejects with [Error] when thelake is slow;
|
|
161
|
+
// spans may already have been accepted via BatchSpanProcessor.
|
|
162
|
+
// Log and continue so session lifecycle is not blocked on OTLP.
|
|
163
|
+
if (event.type === "session.idle" &&
|
|
164
|
+
(Array.isArray(error) ||
|
|
165
|
+
(error instanceof Error && /timed out/i.test(error.message)))) {
|
|
166
|
+
log("warn", `flush after session.idle: ${formatHookError(error)}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}),
|
|
172
|
+
"chat.message": (input, output) => safeRun("chat.message", () => {
|
|
173
|
+
tracer.traceUserMessage({
|
|
174
|
+
sessionID: input.sessionID,
|
|
175
|
+
messageID: input.messageID,
|
|
176
|
+
agent: input.agent,
|
|
177
|
+
model: input.model,
|
|
178
|
+
parts: output.parts,
|
|
179
|
+
});
|
|
180
|
+
}),
|
|
181
|
+
"tool.execute.before": (input, output) => safeRun("tool.execute.before", () => {
|
|
182
|
+
tracer.traceToolStart({
|
|
183
|
+
sessionID: input.sessionID,
|
|
184
|
+
callID: input.callID,
|
|
185
|
+
tool: input.tool,
|
|
186
|
+
args: output.args,
|
|
187
|
+
});
|
|
188
|
+
}),
|
|
189
|
+
"tool.execute.after": (input, output) => safeRun("tool.execute.after", () => {
|
|
190
|
+
tracer.traceToolEnd({
|
|
191
|
+
sessionID: input.sessionID,
|
|
192
|
+
callID: input.callID,
|
|
193
|
+
tool: input.tool,
|
|
194
|
+
args: input.args,
|
|
195
|
+
title: output.title,
|
|
196
|
+
output: output.output,
|
|
197
|
+
});
|
|
198
|
+
}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export const SoftprobePlugin = async () => {
|
|
202
|
+
let tracer;
|
|
203
|
+
try {
|
|
204
|
+
const credentials = await loadSoftprobeCredentials();
|
|
205
|
+
tracer = createSoftprobeSessionTracer({
|
|
206
|
+
publicKey: credentials.publicKey,
|
|
207
|
+
baseUrl: credentials.baseUrl,
|
|
208
|
+
otlpEndpoint: credentials.otlpEndpoint,
|
|
209
|
+
environment: credentials.environment,
|
|
210
|
+
userId: credentials.userId,
|
|
211
|
+
serviceName: credentials.serviceName ?? "opencode",
|
|
212
|
+
});
|
|
213
|
+
log("info", `OTLP tracing initialized → ${credentials.otlpEndpoint}`);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error instanceof MissingSoftprobeCredentialsError) {
|
|
217
|
+
log("warn", `[Tracing disabled] ${error.message}`);
|
|
218
|
+
return {};
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return createHooksFromTracer(tracer);
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* OpenCode V1 plugin shape (`{ id, server }`).
|
|
226
|
+
* Required so file:// installs do not fall through to legacy export scanning,
|
|
227
|
+
* which would invoke every exported function/class (including Error subclasses).
|
|
228
|
+
*/
|
|
229
|
+
export default {
|
|
230
|
+
id: "softprobe-opencode",
|
|
231
|
+
server: SoftprobePlugin,
|
|
232
|
+
};
|
|
233
|
+
export { SoftprobeSessionTracer, createSoftprobeSessionTracer, } from "./softprobe.js";
|
|
234
|
+
export { loadSoftprobeCredentials, MissingSoftprobeCredentialsError, defaultConfigDir, defaultConfigPath, } from "./config.js";
|
|
235
|
+
export { inferToolKind } from "./tool-kind.js";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { SoftprobeClient, type Generation, type JsonValue, type Observation, type SoftprobeClientOptions } from "@softprobe/tracing";
|
|
2
|
+
import type { AssistantGenerationInput, MessagePart, SessionErrorInfo, UserMessageInput } from "./types.js";
|
|
3
|
+
export type SoftprobeSessionTracerOptions = SoftprobeClientOptions;
|
|
4
|
+
/**
|
|
5
|
+
* Maps OpenCode session hooks to Softprobe Part A observations.
|
|
6
|
+
* Construct with an injected SoftprobeClient (tests) or via {@link createSoftprobeSessionTracer}.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SoftprobeSessionTracer {
|
|
9
|
+
readonly client: SoftprobeClient;
|
|
10
|
+
private readonly userId?;
|
|
11
|
+
private readonly abortedSessions;
|
|
12
|
+
private readonly tracedMessageIds;
|
|
13
|
+
private readonly tracedGenerationIds;
|
|
14
|
+
private readonly tracedEventIds;
|
|
15
|
+
private readonly tracedReasoningIds;
|
|
16
|
+
private readonly tracedToolCallIds;
|
|
17
|
+
private readonly pendingReasoningByMessageId;
|
|
18
|
+
private readonly assistantParts;
|
|
19
|
+
private readonly generationByMessageId;
|
|
20
|
+
private readonly turnByMessageId;
|
|
21
|
+
private readonly latestTurnBySession;
|
|
22
|
+
private readonly activeTools;
|
|
23
|
+
private readonly activeGenerations;
|
|
24
|
+
private readonly generationParents;
|
|
25
|
+
constructor(client: SoftprobeClient, options?: {
|
|
26
|
+
userId?: string;
|
|
27
|
+
});
|
|
28
|
+
clearTraceState(): void;
|
|
29
|
+
endActiveToolObservations(sessionID?: string, error?: SessionErrorInfo): void;
|
|
30
|
+
endActiveGenerationSteps(sessionID?: string, error?: SessionErrorInfo): void;
|
|
31
|
+
endActiveTurnObservations(): void;
|
|
32
|
+
finalizeSessionTracing(): void;
|
|
33
|
+
forceFlush(): Promise<void>;
|
|
34
|
+
shutdown(): Promise<void>;
|
|
35
|
+
private getTurn;
|
|
36
|
+
private generationParent;
|
|
37
|
+
private sessionAttrs;
|
|
38
|
+
traceUserMessage(input: UserMessageInput): void;
|
|
39
|
+
startActiveGenerationStep(input: {
|
|
40
|
+
sessionID: string;
|
|
41
|
+
agent: string;
|
|
42
|
+
model: {
|
|
43
|
+
providerID: string;
|
|
44
|
+
id: string;
|
|
45
|
+
variant?: string;
|
|
46
|
+
};
|
|
47
|
+
started: number;
|
|
48
|
+
snapshot?: string;
|
|
49
|
+
}): void;
|
|
50
|
+
rememberAssistantPart(part: MessagePart): void;
|
|
51
|
+
private getAssistantText;
|
|
52
|
+
private getAssistantToolCalls;
|
|
53
|
+
private flushPendingReasoning;
|
|
54
|
+
traceGeneration(input: AssistantGenerationInput): void;
|
|
55
|
+
traceFailedGenerationStep(input: {
|
|
56
|
+
id: string;
|
|
57
|
+
sessionID: string;
|
|
58
|
+
completed: number;
|
|
59
|
+
error: {
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
62
|
+
}): void;
|
|
63
|
+
traceEvent(input: {
|
|
64
|
+
id: string;
|
|
65
|
+
sessionID: string;
|
|
66
|
+
name: string;
|
|
67
|
+
timestamp: number;
|
|
68
|
+
input?: unknown;
|
|
69
|
+
output?: unknown;
|
|
70
|
+
metadata?: Record<string, JsonValue>;
|
|
71
|
+
parent?: Observation | Generation;
|
|
72
|
+
}): void;
|
|
73
|
+
traceReasoning(input: {
|
|
74
|
+
reasoningID: string;
|
|
75
|
+
sessionID: string;
|
|
76
|
+
timestamp: number;
|
|
77
|
+
text: string;
|
|
78
|
+
messageID?: string;
|
|
79
|
+
source: string;
|
|
80
|
+
parent?: Generation;
|
|
81
|
+
}): void;
|
|
82
|
+
traceReasoningPart(part: MessagePart): void;
|
|
83
|
+
/** Fallback when tools arrive before session.next.step.started. */
|
|
84
|
+
private ensureGenerationParent;
|
|
85
|
+
/**
|
|
86
|
+
* Prefer an active/synthetic generation under the agent turn
|
|
87
|
+
* (`agent → generation → tool`). Creates a generation when the turn exists
|
|
88
|
+
* but session.next.step.started never fired.
|
|
89
|
+
*/
|
|
90
|
+
private toolParent;
|
|
91
|
+
/**
|
|
92
|
+
* Capture tool observations from message.part.updated when tool.execute.*
|
|
93
|
+
* hooks are missed (or spans from those hooks were dropped on export).
|
|
94
|
+
*/
|
|
95
|
+
traceToolPart(part: MessagePart): void;
|
|
96
|
+
traceToolStart(input: {
|
|
97
|
+
sessionID: string;
|
|
98
|
+
callID: string;
|
|
99
|
+
tool: string;
|
|
100
|
+
args: unknown;
|
|
101
|
+
started?: number;
|
|
102
|
+
}): void;
|
|
103
|
+
traceToolEnd(input: {
|
|
104
|
+
sessionID: string;
|
|
105
|
+
callID: string;
|
|
106
|
+
tool: string;
|
|
107
|
+
args: unknown;
|
|
108
|
+
title: string;
|
|
109
|
+
output: string;
|
|
110
|
+
completed?: number;
|
|
111
|
+
started?: number;
|
|
112
|
+
status?: "ok" | "error" | "cancelled";
|
|
113
|
+
}): void;
|
|
114
|
+
traceSessionError(input: {
|
|
115
|
+
sessionID: string;
|
|
116
|
+
error?: SessionErrorInfo;
|
|
117
|
+
}): void;
|
|
118
|
+
}
|
|
119
|
+
export declare function createSoftprobeSessionTracer(options: SoftprobeSessionTracerOptions): SoftprobeSessionTracer;
|