Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,21 @@ Config resolves in this order (later overrides earlier): defaults → `~/.cursor

Every `LANGSMITH_CURSOR_*` variable also accepts the `LANGSMITH_*` form (the `LANGSMITH_CURSOR_*` name wins when both are set).

| Environment variable | Config key | Description | Default |
| --------------------------------- | ---------------- | --------------------------------------------------------------------- | --------------------------------- |
| `TRACE_TO_LANGSMITH` | `enabled` | Master switch — tracing runs only when truthy. | `false` |
| `LANGSMITH_CURSOR_API_KEY` | `api_key` | LangSmith API key. | — |
| `LANGSMITH_CURSOR_ENDPOINT` | `api_url` | LangSmith API base URL. | `https://api.smith.langchain.com` |
| `LANGSMITH_CURSOR_PROJECT` | `project` | Target tracing project. | `cursor` |
| `LANGSMITH_CURSOR_METADATA` | `metadata` | Extra metadata attached to every run (JSON object). | — |
| `LANGSMITH_CURSOR_RUNS_ENDPOINTS` | `replicas` | Additional replica destinations (JSON array). | — |
| `LANGSMITH_CURSOR_ATTACHMENTS` | `attachments` | Enrich turns with image/file attachment bytes from Cursor's DB. | `true` |
| `LANGSMITH_CURSOR_DB_PATH` | `cursor_db_path` | Override the Cursor `state.vscdb` path used for attachments. | platform default |
| `LANGSMITH_CURSOR_REDACT` | `redact` | Redact detected secrets from traced data before upload. | `true` |
| Environment variable | Config key | Description | Default |
| --------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| `TRACE_TO_LANGSMITH` | `enabled` | Master switch — tracing runs only when truthy. | `false` |
| `LANGSMITH_CURSOR_API_KEY` | `api_key` | LangSmith API key. | — |
| `LANGSMITH_CURSOR_ENDPOINT` | `api_url` | LangSmith API base URL. | `https://api.smith.langchain.com` |
| `LANGSMITH_CURSOR_PROJECT` | `project` | Target tracing project. | `cursor` |
| `LANGSMITH_CURSOR_METADATA` | `metadata` | Extra metadata attached to every run (JSON object). | — |
| `LANGSMITH_CURSOR_RUNS_ENDPOINTS` | `replicas` | Additional replica destinations (JSON array). | — |
| `LANGSMITH_CURSOR_ATTACHMENTS` | `attachments` | Enrich turns with image/file attachment bytes from Cursor's DB. | `true` |
| `LANGSMITH_CURSOR_DB_PATH` | `cursor_db_path` | Override the Cursor `state.vscdb` path used for attachments. | platform default |
| `LANGSMITH_CURSOR_REDACT` | `redact` | Redact detected secrets from traced data before upload. | `true` |
| `LANGSMITH_CURSOR_REDACT_EXTRA` | — | Extra redaction rules: JSON array of `{ pattern, replace }`; each `pattern` is case-sensitive and applied with the `g` flag. | — |
| `LANGSMITH_CURSOR_DEBUG` | — | Verbose hook logging. | `false` |
| `LANGSMITH_CURSOR_STATE_FILE` | — | Override the on-disk event-buffer state file (no `LANGSMITH_*` form). | `~/.cursor/langsmith-state.json` |
| `LANGSMITH_CURSOR_LOG_FILE` | — | Override the hook log file (no `LANGSMITH_*` form). | `~/.cursor/langsmith-hook.log` |
| `LANGSMITH_CURSOR_DEBUG` | — | Verbose hook logging. | `false` |
| `LANGSMITH_CURSOR_STATE_FILE` | — | Override the on-disk event-buffer state file (no `LANGSMITH_*` form). | `~/.cursor/langsmith-state.json` |
| `LANGSMITH_CURSOR_LOG_FILE` | — | Override the hook log file (no `LANGSMITH_*` form). | `~/.cursor/langsmith-hook.log` |

Tracing only runs when `enabled` (or `TRACE_TO_LANGSMITH=true`) **and** an API key (or replicas) is set.

Expand All @@ -110,7 +110,7 @@ We don't compute cost locally. Instead, Cursor's model labels (e.g. `claude-4.6-

Every run carries the shared [`coding-agent-v1`](https://github.com/langchain-ai/langsmith) coding-agent metadata contract on `run.extra.metadata`, built by one helper (`src/metadata.ts`) and propagated to child runs. This lets traces from any coding agent (Claude Code, Codex, Cursor, …) be identified, grouped, and attributed with the same stable keys.

**Always present** (every run): `ls_agent_kind` (`"coding_agent"`), `ls_integration` (`"cursor"`), `ls_agent_runtime` (`"Cursor"`), `ls_trace_schema_version` (`"coding-agent-v1"`), `thread_id` (= `conversation_id`).
**Always present** (every run): `ls_agent_purpose` (`"coding"`), `ls_agent_type` (`"root"` or `"subagent"` based on the owning agent), `ls_integration` (`"cursor"`), `ls_agent_runtime` (`"Cursor"`), `ls_trace_schema_version` (`"coding-agent-v1"`), `thread_id` (= `conversation_id`).

**Present where known** (every run): `ls_integration_version` (plugin version, build-time injected), `ls_agent_runtime_version` (Cursor's `cursor_version`), `turn_id` (= `generation_id`), `turn_number`, `repository_url` / `repository_provider` / `repository_name`, `git_branch`, `git_commit_sha`, `cwd`.

Expand Down
36 changes: 27 additions & 9 deletions bundle/stop.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ function parseJson<T = Record<string, unknown>>(value: unknown): T | undefined {
function isRedactRule(rule: unknown): rule is StringNodeRule {
if (typeof rule !== "object" || rule === null) return false;
const r = rule as Record<string, unknown>;
return typeof r.pattern === "string" && (r.replace === undefined || typeof r.replace === "string");
return (
typeof r.pattern === "string" && (r.replace === undefined || typeof r.replace === "string")
);
}

/** Parse a JSON array of { pattern, replace }; invalid rules are logged and skipped. */
Expand Down Expand Up @@ -256,8 +258,7 @@ export function loadConfig(options?: { cwd?: string }): Config {
true;
const cursorDbPath = getEnv("DB_PATH") ?? localFile?.cursor_db_path ?? globalFile?.cursor_db_path;

const redact =
parseBoolean(getEnv("REDACT")) ?? localFile?.redact ?? globalFile?.redact ?? true;
const redact = parseBoolean(getEnv("REDACT")) ?? localFile?.redact ?? globalFile?.redact ?? true;
const redactExtraRules = parseRedactExtraRules(getEnv("REDACT_EXTRA"));

const stateFilePath =
Expand Down
13 changes: 10 additions & 3 deletions src/conversation-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,10 @@ export function decodeStep(buf: Buffer): Step | undefined {
}
const assistant = firstBytes(buf, STEP_ASSISTANT_FIELD);
if (assistant) {
return { kind: "assistant", text: firstBytes(assistant, MESSAGE_TEXT_FIELD)?.toString("utf-8") };
return {
kind: "assistant",
text: firstBytes(assistant, MESSAGE_TEXT_FIELD)?.toString("utf-8"),
};
}
return undefined;
}
Expand Down Expand Up @@ -314,7 +317,9 @@ export function resolveTurnSteps(opts: ResolveTurnStepsOptions): Step[] | undefi
for (let i = turnIds.length - 1; i >= 0; i--) {
const steps = decodeTurnSteps(reader, turnIds[i]);
if (!steps) continue;
const overlap = steps.some((s) => s.kind === "tool" && s.toolUseId && wanted.has(s.toolUseId));
const overlap = steps.some(
(s) => s.kind === "tool" && s.toolUseId && wanted.has(s.toolUseId),
);
if (overlap) {
logger.log(
`conversation-steps: recovered ${steps.length} step(s) for ${opts.conversationId}`,
Expand All @@ -328,7 +333,9 @@ export function resolveTurnSteps(opts: ResolveTurnStepsOptions): Step[] | undefi
reader.close();
}
} catch (err) {
logger.warn(`conversation-steps: resolution failed for ${opts.conversationId}, skipping (${err})`);
logger.warn(
`conversation-steps: resolution failed for ${opts.conversationId}, skipping (${err})`,
);
return undefined;
}
}
27 changes: 21 additions & 6 deletions src/langsmith.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { StringNodeRule } from "langsmith/anonymizer";
import type { TurnBuffer, ToolEvent, SubagentEvent, ContentPart } from "./types.js";
import { buildUsageMetadata, deriveModelInfo } from "./normalize.js";
import { DEFAULT_TAGS, TURN_RUN_NAME } from "./constants.js";
import { codingAgentMetadata } from "./metadata.js";
import { codingAgentMetadata, type LSAgentType } from "./metadata.js";
import { groupSteps, type Step } from "./conversation-steps.js";
import * as logger from "./logger.js";

Expand Down Expand Up @@ -101,6 +101,7 @@ export interface BuildTurnOptions {

/** Per-turn context shared by every run's coding-agent-v1 metadata. */
interface MetaCtx {
agentType: LSAgentType;
threadId: string;
base?: Record<string, unknown>;
turnId?: string;
Expand Down Expand Up @@ -192,6 +193,7 @@ export async function buildTurnRuns(options: BuildTurnOptions): Promise<void> {
// coding-agent-v1 context, stamped on the root and propagated to children
// via createChild. user_email (per-turn) joins the config base.
const ctx: MetaCtx = {
agentType: "root",
threadId: conversationId,
base: { ...customMetadata, ...(userEmail ? { user_email: userEmail } : {}) },
turnId: buffer.generation_id,
Expand Down Expand Up @@ -497,6 +499,7 @@ async function postSubagentRun(sub: SubagentEvent, parent: RunTree, ctx: MetaCtx
ls_model_name: subModel.ls_model_name,
ls_invocation_params: { model: subModel.ls_model_name },
};
const subagentCtx: MetaCtx = { ...ctx, agentType: "subagent" };

// Subagent = nested chain run (validator runType "subagent"); children clear
// ls_subagent_id/type so they don't leak down.
Expand All @@ -517,7 +520,7 @@ async function postSubagentRun(sub: SubagentEvent, parent: RunTree, ctx: MetaCtx
end_time: endMs,
extra: {
metadata: codingAgentMetadata({
...ctx,
...subagentCtx,
subagentId: sub.subagent_id,
subagentType: sub.subagent_type,
runSpecific: {
Expand Down Expand Up @@ -554,7 +557,11 @@ async function postSubagentRun(sub: SubagentEvent, parent: RunTree, ctx: MetaCtx
start_time: startMs,
end_time: endMs,
extra: {
metadata: codingAgentMetadata({ ...ctx, clearSubagent: true, runSpecific: { ...llmMeta } }),
metadata: codingAgentMetadata({
...subagentCtx,
clearSubagent: true,
runSpecific: { ...llmMeta },
}),
},
});
await llmRun.postRun();
Expand All @@ -574,12 +581,16 @@ async function postSubagentRun(sub: SubagentEvent, parent: RunTree, ctx: MetaCtx
start_time: startMs,
end_time: Math.max(startMs, firstCallStart),
extra: {
metadata: codingAgentMetadata({ ...ctx, clearSubagent: true, runSpecific: { ...llmMeta } }),
metadata: codingAgentMetadata({
...subagentCtx,
clearSubagent: true,
runSpecific: { ...llmMeta },
}),
},
});
await decideRun.postRun();

for (const tool of tools) await postToolRun(tool, subagentRun, ctx, true);
for (const tool of tools) await postToolRun(tool, subagentRun, subagentCtx, true);

const answerRun = subagentRun.createChild({
name: llmName,
Expand All @@ -595,7 +606,11 @@ async function postSubagentRun(sub: SubagentEvent, parent: RunTree, ctx: MetaCtx
start_time: lastCallEnd,
end_time: endMs,
extra: {
metadata: codingAgentMetadata({ ...ctx, clearSubagent: true, runSpecific: { ...llmMeta } }),
metadata: codingAgentMetadata({
...subagentCtx,
clearSubagent: true,
runSpecific: { ...llmMeta },
}),
},
});
await answerRun.postRun();
Expand Down
14 changes: 9 additions & 5 deletions src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@

// ─── Frozen literals (identity block) ────────────────────────────────────────

export const LS_AGENT_KIND = "coding_agent";
export const LS_AGENT_PURPOSE = "coding";
export type LSAgentType = "root" | "subagent" | "middleware" | "compaction";
export const LS_INTEGRATION = "cursor";
export const LS_AGENT_RUNTIME = "Cursor";
export const LS_TRACE_SCHEMA_VERSION = "coding-agent-v1";

// ─── Helper input ─────────────────────────────────────────────────────────────

export interface CodingAgentMetadataOptions {
/** Role of the run within the coding-agent trace. Required on every run. */
agentType: LSAgentType;

/** Stable conversation id → `thread_id`. Required on every run. */
threadId: string;

Expand Down Expand Up @@ -50,10 +54,9 @@ export interface CodingAgentMetadataOptions {
* Build the coding-agent-v1 metadata for one run. Merge order (later wins):
* identity → dynamic → runSpecific → base.
*/
export function codingAgentMetadata(
opts: CodingAgentMetadataOptions,
): Record<string, unknown> {
export function codingAgentMetadata(opts: CodingAgentMetadataOptions): Record<string, unknown> {
const {
agentType,
threadId,
base,
turnId,
Expand All @@ -70,7 +73,8 @@ export function codingAgentMetadata(

const meta: Record<string, unknown> = {
// Identity & grouping — always present.
ls_agent_kind: LS_AGENT_KIND,
ls_agent_purpose: LS_AGENT_PURPOSE,
ls_agent_type: agentType,
ls_integration: LS_INTEGRATION,
ls_agent_runtime: LS_AGENT_RUNTIME,
ls_trace_schema_version: LS_TRACE_SCHEMA_VERSION,
Expand Down
8 changes: 4 additions & 4 deletions src/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ export function readProtoLenField(buf: Buffer, field: number): Buffer[] {
/** Decode a composerData.conversationState value: "~"-prefixed base64, else hex. */
export function decodeConversationStateBlob(raw: unknown): Buffer | undefined {
if (typeof raw !== "string" || raw.length === 0) return undefined;
const buf = raw.startsWith("~")
? Buffer.from(raw.slice(1), "base64")
: Buffer.from(raw, "hex");
const buf = raw.startsWith("~") ? Buffer.from(raw.slice(1), "base64") : Buffer.from(raw, "hex");
return buf.length > 0 ? buf : undefined;
}

Expand Down Expand Up @@ -125,7 +123,9 @@ function systemPromptFor(reader: BlobReader, conversationId: string): string | u
if (!composer) return undefined;

const parsed = JSON.parse(composer.toString("utf-8"));
const blob = decodeConversationStateBlob(isRecord(parsed) ? parsed.conversationState : undefined);
const blob = decodeConversationStateBlob(
isRecord(parsed) ? parsed.conversationState : undefined,
);
if (!blob) return undefined;

for (const id of readProtoLenField(blob, ROOT_PROMPT_MESSAGES_FIELD)) {
Expand Down
Loading