feat: TypeScript SDK (agentlens-sdk) and OpenCode plugin (opencode-agentlens)

- packages/sdk-ts: BatchTransport, TraceBuilder, models, decision helpers
  Zero external deps, native fetch, ESM+CJS output
- packages/opencode-plugin: OpenCode plugin with hooks for:
  - Session lifecycle (create/idle/error/delete/diff)
  - Tool execution capture (before/after -> TOOL_CALL spans + TOOL_SELECTION decisions)
  - LLM call tracking (chat.message -> LLM_CALL spans with model/provider)
  - Permission flow (permission.ask -> ESCALATION decisions)
  - File edit events
  - Model cost estimation (Claude, GPT-4o, o3-mini pricing)
This commit is contained in:
Vectry
2026-02-10 03:08:51 +00:00
parent 0149e0a6f4
commit 6bed493275
17 changed files with 2589 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import type { Plugin } from "@opencode-ai/plugin";
import type { JsonValue } from "agentlens-sdk";
import { init, flush, EventType as EventTypeValues } from "agentlens-sdk";
import { loadConfig } from "./config.js";
import { SessionState } from "./state.js";
import { truncate, safeJsonValue } from "./utils.js";
const plugin: Plugin = async ({ project, directory, worktree }) => {
const config = loadConfig();
if (!config.enabled || !config.apiKey) {
console.log("[agentlens] Plugin disabled — missing AGENTLENS_API_KEY");
return {};
}
init({
apiKey: config.apiKey,
endpoint: config.endpoint,
flushInterval: config.flushInterval,
maxBatchSize: config.maxBatchSize,
});
const state = new SessionState();
return {
event: async ({ event }) => {
const type = event.type;
const props = (event as Record<string, unknown>).properties as
| Record<string, unknown>
| undefined;
if (type === "session.created" && props?.["id"]) {
state.startSession(String(props["id"]), {
project: project.id,
directory,
worktree,
});
}
if (type === "session.idle") {
const sessionId = props?.["sessionID"] ?? props?.["id"];
if (sessionId) await flush();
}
if (type === "session.error") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
if (sessionId) {
const trace = state.getTrace(sessionId);
if (trace) {
trace.addEvent({
type: EventTypeValues.ERROR,
name: String(props?.["error"] ?? "session error"),
metadata: safeJsonValue(props) as JsonValue,
});
}
}
}
if (type === "session.deleted") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
if (sessionId) state.endSession(sessionId);
}
if (type === "session.diff") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
if (sessionId) {
const trace = state.getTrace(sessionId);
if (trace) {
trace.setMetadata({
diff: truncate(String(props?.["diff"] ?? ""), 5000),
});
}
}
}
if (type === "file.edited") {
const sessionId = String(props?.["sessionID"] ?? props?.["id"] ?? "");
const trace = sessionId ? state.getTrace(sessionId) : undefined;
if (trace) {
trace.addEvent({
type: EventTypeValues.CUSTOM,
name: "file.edited",
metadata: safeJsonValue({
filePath: props?.["filePath"],
}) as JsonValue,
});
}
}
},
"tool.execute.before": async (input, output) => {
state.startToolCall(
input.callID,
input.tool,
output.args as unknown,
input.sessionID,
);
},
"tool.execute.after": async (input, output) => {
state.endToolCall(
input.callID,
truncate(output.output ?? "", config.maxOutputLength),
output.title ?? input.tool,
output.metadata as unknown,
);
},
"chat.message": async (input) => {
if (input.model) {
state.recordLLMCall(input.sessionID, {
model: input.model,
agent: input.agent,
messageID: input.messageID,
});
}
},
"chat.params": async (input, output) => {
const trace = state.getTrace(input.sessionID);
if (trace) {
trace.addEvent({
type: EventTypeValues.CUSTOM,
name: "chat.params",
metadata: safeJsonValue({
agent: input.agent,
model: input.model.id,
provider: input.provider.info.id,
temperature: output.temperature,
topP: output.topP,
topK: output.topK,
}) as JsonValue,
});
}
},
"permission.ask": async (input, output) => {
state.recordPermission(input.sessionID, input, output.status);
},
};
};
export default plugin;
export { plugin as AgentLensPlugin };
export type { PluginConfig } from "./config.js";
export { loadConfig } from "./config.js";