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:
146
packages/opencode-plugin/src/index.ts
Normal file
146
packages/opencode-plugin/src/index.ts
Normal 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";
|
||||
Reference in New Issue
Block a user