From 19c36acfa297dfcc8c41d1a6ab4749a8ebb26f51 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 15 Jun 2026 15:08:36 -0700 Subject: [PATCH 1/4] feat(copilot): server-side mothership tool/vfs/file metrics + $env tagging - copilot.tool.duration/calls, vfs.materialize, file.read metrics on Sim's MeterProvider (shared buckets, bounded labels, name-capped) at the tool/VFS/file-read boundaries - metrics-v1 TS mirror + sync-metrics-contract script + GENERATORS entry; regen trace-attributes/events mirrors (adds gen_ai.system) - deployment.environment from APPCONFIG_ENVIRONMENT (production -> prod) so a single Grafana $env spans Sim + Go without a new infra env var --- apps/sim/instrumentation-node.ts | 19 ++- apps/sim/lib/copilot/generated/metrics-v1.ts | 58 +++++++ .../copilot/generated/trace-attributes-v1.ts | 50 ++++-- .../lib/copilot/generated/trace-events-v1.ts | 4 +- apps/sim/lib/copilot/request/metrics.ts | 101 +++++++++++++ .../sim/lib/copilot/request/tools/executor.ts | 7 + apps/sim/lib/copilot/vfs/file-reader.ts | 9 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 13 +- package.json | 2 + scripts/generate-mship-contracts.ts | 1 + scripts/sync-metrics-contract.ts | 142 ++++++++++++++++++ 11 files changed, 390 insertions(+), 16 deletions(-) create mode 100644 apps/sim/lib/copilot/generated/metrics-v1.ts create mode 100644 apps/sim/lib/copilot/request/metrics.ts create mode 100644 scripts/sync-metrics-contract.ts diff --git a/apps/sim/instrumentation-node.ts b/apps/sim/instrumentation-node.ts index e52eb884a9e..9d536bb5276 100644 --- a/apps/sim/instrumentation-node.ts +++ b/apps/sim/instrumentation-node.ts @@ -83,6 +83,17 @@ function normalizeOtlpMetricsUrl(url: string): string { } } +// deployment.environment in the GO value space (dev | staging | prod) without +// any new infra env var. Every deployed Sim tier already gets +// APPCONFIG_ENVIRONMENT = the infra env name (dev | staging | production), so we +// reuse it and map production -> prod to match Go's `OTEL_DEPLOYMENT_ENVIRONMENT` +// (and thus a single Grafana $env filter spans Sim + Go). Returns undefined when +// unset (local dev) so the OTEL_/NODE_ENV fallbacks still apply. +function deploymentEnvFromAppConfig(v: string | undefined): string | undefined { + if (!v) return undefined + return v === 'production' ? 'prod' : v +} + // Sampling ratio from env (mirrors Go's `samplerFromEnv`); fallback // is 100% everywhere. Retention caps cost, not sampling. function resolveSamplingRatio(_isLocalEndpoint: boolean): number { @@ -270,11 +281,15 @@ async function initializeOpenTelemetry() { resourceFromAttributes({ [ATTR_SERVICE_NAME]: telemetryConfig.serviceName, [ATTR_SERVICE_VERSION]: telemetryConfig.serviceVersion, - // OTEL_ → DEPLOYMENT_ENVIRONMENT → NODE_ENV; matches Go's - // `resourceEnvFromEnv()` so both halves tag the same value. + // OTEL_ → DEPLOYMENT_ENVIRONMENT → APPCONFIG_ENVIRONMENT (mapped to the + // Go value space) → NODE_ENV. Matches Go's `resourceEnvFromEnv()` so a + // single $env spans Sim + Go. APPCONFIG_ENVIRONMENT (already set on every + // deployed tier) is the fix that stops deployed Sim tagging everything + // "production" via the NODE_ENV fallback — no new infra env var needed. [ATTR_DEPLOYMENT_ENVIRONMENT]: process.env.OTEL_DEPLOYMENT_ENVIRONMENT || process.env.DEPLOYMENT_ENVIRONMENT || + deploymentEnvFromAppConfig(process.env.APPCONFIG_ENVIRONMENT) || env.NODE_ENV || 'development', 'service.namespace': 'mothership', diff --git a/apps/sim/lib/copilot/generated/metrics-v1.ts b/apps/sim/lib/copilot/generated/metrics-v1.ts new file mode 100644 index 00000000000..dd8527f8158 --- /dev/null +++ b/apps/sim/lib/copilot/generated/metrics-v1.ts @@ -0,0 +1,58 @@ +// AUTO-GENERATED FILE. DO NOT EDIT. +// +// Source: copilot/copilot/contracts/metrics-v1.schema.json +// Regenerate with: bun run metrics-contract:generate +// +// Canonical mothership OTel metric names. Call sites should reference +// `Metric.` (e.g. `Metric.CopilotToolDuration`) rather than raw +// string literals, so the Go-side contract is the single source of truth and +// typos become compile errors. +// +// NAMES ONLY. Label keys and histogram bucket boundaries are NOT in this +// contract — Go owns the label-cardinality allowlist and the shared bucket +// constant, and the Sim emitter MUST mirror those by hand so the Go∪Sim metric +// union is queryable as one series set. + +export const Metric = { + CopilotCacheAttempted: 'copilot.cache.attempted', + CopilotCacheHit: 'copilot.cache.hit', + CopilotCacheWrite: 'copilot.cache.write', + CopilotFileReadDuration: 'copilot.file.read.duration', + CopilotFileReadSize: 'copilot.file.read.size', + CopilotMessagesSerializeDuration: 'copilot.messages.serialize.duration', + CopilotRequestCount: 'copilot.request.count', + CopilotRequestDuration: 'copilot.request.duration', + CopilotToolCalls: 'copilot.tool.calls', + CopilotToolDuration: 'copilot.tool.duration', + CopilotVfsMaterializeDuration: 'copilot.vfs.materialize.duration', + GenAiClientCacheTokenUsage: 'gen_ai.client.cache.token.usage', + GenAiClientTokenUsage: 'gen_ai.client.token.usage', + LlmClientErrors: 'llm.client.errors', + LlmClientOutputCutoff: 'llm.client.output_cutoff', + LlmClientStreamDuration: 'llm.client.stream.duration', + LlmClientTimeToFirstToken: 'llm.client.time_to_first_token', +} as const + +export type MetricKey = keyof typeof Metric +export type MetricValue = (typeof Metric)[MetricKey] + +/** Readonly sorted list of every canonical mothership metric name. */ +export const MetricValues: readonly MetricValue[] = [ + 'copilot.cache.attempted', + 'copilot.cache.hit', + 'copilot.cache.write', + 'copilot.file.read.duration', + 'copilot.file.read.size', + 'copilot.messages.serialize.duration', + 'copilot.request.count', + 'copilot.request.duration', + 'copilot.tool.calls', + 'copilot.tool.duration', + 'copilot.vfs.materialize.duration', + 'gen_ai.client.cache.token.usage', + 'gen_ai.client.token.usage', + 'llm.client.errors', + 'llm.client.output_cutoff', + 'llm.client.stream.duration', + 'llm.client.time_to_first_token', +] as const diff --git a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts index 441ec59d16d..982857673f5 100644 --- a/apps/sim/lib/copilot/generated/trace-attributes-v1.ts +++ b/apps/sim/lib/copilot/generated/trace-attributes-v1.ts @@ -54,10 +54,8 @@ export const TraceAttr = { AuthProvider: 'auth.provider', AuthValidateStatusCode: 'auth.validate.status_code', AwsRegion: 'aws.region', - BedrockErrorCode: 'bedrock.error_code', - BedrockModelId: 'bedrock.model_id', - BedrockRequestBodyBytesRetry: 'bedrock.request.body_bytes_retry', BillingAttempts: 'billing.attempts', + BillingByok: 'billing.byok', BillingChangeType: 'billing.change_type', BillingCostInputUsd: 'billing.cost.input_usd', BillingCostOutputUsd: 'billing.cost.output_usd', @@ -159,6 +157,14 @@ export const TraceAttr = { ContextReduced: 'context.reduced', ContextSummarizeInputChars: 'context.summarize.input_chars', ContextSummarizeOutputChars: 'context.summarize.output_chars', + ContextTransformCaller: 'context.transform.caller', + ContextTransformCharsIn: 'context.transform.chars_in', + ContextTransformCharsOut: 'context.transform.chars_out', + ContextTransformDropCount: 'context.transform.drop_count', + ContextTransformDrops: 'context.transform.drops', + ContextTransformMessagesIn: 'context.transform.messages_in', + ContextTransformMessagesOut: 'context.transform.messages_out', + ContextTransformStage: 'context.transform.stage', CopilotAbortControllerFired: 'copilot.abort.controller_fired', CopilotAbortGoMarkerOk: 'copilot.abort.go_marker_ok', CopilotAbortLocalAborted: 'copilot.abort.local_aborted', @@ -276,6 +282,7 @@ export const TraceAttr = { CopilotVfsOutcome: 'copilot.vfs.outcome', CopilotVfsOutputBytes: 'copilot.vfs.output.bytes', CopilotVfsOutputMediaType: 'copilot.vfs.output.media_type', + CopilotVfsPhase: 'copilot.vfs.phase', CopilotVfsReadImageResized: 'copilot.vfs.read.image.resized', CopilotVfsReadOutcome: 'copilot.vfs.read.outcome', CopilotVfsReadOutputBytes: 'copilot.vfs.read.output.bytes', @@ -389,6 +396,7 @@ export const TraceAttr = { GenAiRequestToolUseBlocks: 'gen_ai.request.tool_use_blocks', GenAiRequestToolsCount: 'gen_ai.request.tools.count', GenAiRequestUserMessages: 'gen_ai.request.user_messages', + GenAiResponseFinishReasons: 'gen_ai.response.finish_reasons', GenAiResponseModel: 'gen_ai.response.model', GenAiStreamPhaseTextBytes: 'gen_ai.stream.phase.text.bytes', GenAiStreamPhaseTextChunks: 'gen_ai.stream.phase.text.chunks', @@ -434,7 +442,9 @@ export const TraceAttr = { InvitationRole: 'invitation.role', KnowledgeBaseId: 'knowledge_base.id', KnowledgeBaseName: 'knowledge_base.name', + LlmBackend: 'llm.backend', LlmErrorStage: 'llm.error_stage', + LlmProtocol: 'llm.protocol', LlmRequestBodyBytes: 'llm.request.body_bytes', LlmStreamBytes: 'llm.stream.bytes', LlmStreamChunks: 'llm.stream.chunks', @@ -460,6 +470,10 @@ export const TraceAttr = { MemoryPath: 'memory.path', MemoryRowCount: 'memory.row_count', MessageId: 'message.id', + MessagesDeserializeMs: 'messages.deserialize_ms', + MessagesSerializeOp: 'messages.serialize.op', + MessagesSerializeSite: 'messages.serialize.site', + MessagesSerializeMs: 'messages.serialize_ms', MessagingDestinationName: 'messaging.destination.name', MessagingSystem: 'messaging.system', ModelDurationMs: 'model.duration_ms', @@ -495,14 +509,15 @@ export const TraceAttr = { ResumeResultsFailureCount: 'resume.results.failure_count', ResumeResultsSuccessCount: 'resume.results.success_count', RouterBackendName: 'router.backend_name', - RouterBedrockEnabled: 'router.bedrock_enabled', - RouterBedrockSupportedModel: 'router.bedrock_supported_model', + RouterConfigVersion: 'router.config_version', RouterId: 'router.id', RouterName: 'router.name', + RouterRouteReason: 'router.route_reason', RouterSelectedBackend: 'router.selected_backend', RouterSelectedPath: 'router.selected_path', RunId: 'run.id', SearchResultsCount: 'search.results_count', + ServerAddress: 'server.address', ServiceInstanceId: 'service.instance.id', ServiceName: 'service.name', ServiceNamespace: 'service.namespace', @@ -663,10 +678,8 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'auth.provider', 'auth.validate.status_code', 'aws.region', - 'bedrock.error_code', - 'bedrock.model_id', - 'bedrock.request.body_bytes_retry', 'billing.attempts', + 'billing.byok', 'billing.change_type', 'billing.cost.input_usd', 'billing.cost.output_usd', @@ -768,6 +781,14 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'context.reduced', 'context.summarize.input_chars', 'context.summarize.output_chars', + 'context.transform.caller', + 'context.transform.chars_in', + 'context.transform.chars_out', + 'context.transform.drop_count', + 'context.transform.drops', + 'context.transform.messages_in', + 'context.transform.messages_out', + 'context.transform.stage', 'copilot.abort.controller_fired', 'copilot.abort.go_marker_ok', 'copilot.abort.local_aborted', @@ -885,6 +906,7 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'copilot.vfs.outcome', 'copilot.vfs.output.bytes', 'copilot.vfs.output.media_type', + 'copilot.vfs.phase', 'copilot.vfs.read.image.resized', 'copilot.vfs.read.outcome', 'copilot.vfs.read.output.bytes', @@ -987,6 +1009,7 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'gen_ai.request.tool_use_blocks', 'gen_ai.request.tools.count', 'gen_ai.request.user_messages', + 'gen_ai.response.finish_reasons', 'gen_ai.response.model', 'gen_ai.stream.phase.text.bytes', 'gen_ai.stream.phase.text.chunks', @@ -1032,7 +1055,9 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'invitation.role', 'knowledge_base.id', 'knowledge_base.name', + 'llm.backend', 'llm.error_stage', + 'llm.protocol', 'llm.request.body_bytes', 'llm.stream.bytes', 'llm.stream.chunks', @@ -1058,6 +1083,10 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'memory.path', 'memory.row_count', 'message.id', + 'messages.deserialize_ms', + 'messages.serialize.op', + 'messages.serialize.site', + 'messages.serialize_ms', 'messaging.destination.name', 'messaging.system', 'model.duration_ms', @@ -1093,14 +1122,15 @@ export const TraceAttrValues: readonly TraceAttrValue[] = [ 'resume.results.failure_count', 'resume.results.success_count', 'router.backend_name', - 'router.bedrock_enabled', - 'router.bedrock_supported_model', + 'router.config_version', 'router.id', 'router.name', + 'router.route_reason', 'router.selected_backend', 'router.selected_path', 'run.id', 'search.results_count', + 'server.address', 'service.instance.id', 'service.name', 'service.namespace', diff --git a/apps/sim/lib/copilot/generated/trace-events-v1.ts b/apps/sim/lib/copilot/generated/trace-events-v1.ts index 345606eff40..ffc4c17361d 100644 --- a/apps/sim/lib/copilot/generated/trace-events-v1.ts +++ b/apps/sim/lib/copilot/generated/trace-events-v1.ts @@ -10,7 +10,7 @@ // become compile errors. export const TraceEvent = { - BedrockInvokeRetryWithoutImages: 'bedrock.invoke.retry_without_images', + ContextTransform: 'context.transform', CopilotOutputFileError: 'copilot.output_file.error', CopilotSseFirstEvent: 'copilot.sse.first_event', CopilotSseIdleGapExceeded: 'copilot.sse.idle_gap_exceeded', @@ -33,7 +33,7 @@ export type TraceEventValue = (typeof TraceEvent)[TraceEventKey] /** Readonly sorted list of every canonical event name. */ export const TraceEventValues: readonly TraceEventValue[] = [ - 'bedrock.invoke.retry_without_images', + 'context.transform', 'copilot.output_file.error', 'copilot.sse.first_event', 'copilot.sse.idle_gap_exceeded', diff --git a/apps/sim/lib/copilot/request/metrics.ts b/apps/sim/lib/copilot/request/metrics.ts new file mode 100644 index 00000000000..31f0e7996f6 --- /dev/null +++ b/apps/sim/lib/copilot/request/metrics.ts @@ -0,0 +1,101 @@ +// Sim server-side copilot metrics (U17). Sim's MeterProvider is wired in +// instrumentation-node.ts (OTLP → Mimir, 60s) but had no copilot instruments; +// this module is its first consumer. We emit the SAME metric names + label keys +// + histogram bucket boundaries as the Go side (copilot internal/telemetry + +// contracts/metrics_v1.go) so the Go∪Sim union is queryable as one series set +// — e.g. `copilot.tool.duration` split by `tool.executor` (go|client|sim). +// +// Bounded cardinality only: tool.name is capped to the shared tool catalog +// (else "other"); vfs phase / file-read outcome are bounded sets. NEVER a +// user/chat/request id (those explode Prometheus series). +import { type Counter, type Histogram, metrics } from '@opentelemetry/api' +import { Metric } from '@/lib/copilot/generated/metrics-v1' +import { TOOL_CATALOG } from '@/lib/copilot/generated/tool-catalog-v1' +import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' + +// MUST match Go's copilot/internal/telemetry/metrics.go LatencyBucketsMs +// exactly — a histogram_quantile(sum by (le) …) over the Go∪Sim union is only +// valid with identical boundaries. If you change one side, change the other. +const LATENCY_BUCKETS_MS = [ + 50, 100, 250, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 120000, 300000, +] + +// File sizes span KB→tens of MB; a bytes-appropriate bucket set (not latency). +const BYTE_BUCKETS = [1024, 8192, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 268435456] + +interface CopilotMeterInstruments { + toolDuration: Histogram + toolCalls: Counter + vfsMaterializeDuration: Histogram + fileReadDuration: Histogram + fileReadBytes: Histogram +} + +let cached: CopilotMeterInstruments | undefined + +// Lazy init: Turbopack/Next can evaluate this module before the NodeSDK +// installs the real MeterProvider, so resolve instruments on first use (a +// no-op meter before then simply drops records — same pattern as getCopilotTracer). +function instruments(): CopilotMeterInstruments { + if (cached) return cached + const meter = metrics.getMeter('sim-copilot') + cached = { + toolDuration: meter.createHistogram(Metric.CopilotToolDuration, { + unit: 'ms', + advice: { explicitBucketBoundaries: LATENCY_BUCKETS_MS }, + }), + toolCalls: meter.createCounter(Metric.CopilotToolCalls), + vfsMaterializeDuration: meter.createHistogram(Metric.CopilotVfsMaterializeDuration, { + unit: 'ms', + advice: { explicitBucketBoundaries: LATENCY_BUCKETS_MS }, + }), + fileReadDuration: meter.createHistogram(Metric.CopilotFileReadDuration, { + unit: 'ms', + advice: { explicitBucketBoundaries: LATENCY_BUCKETS_MS }, + }), + fileReadBytes: meter.createHistogram(Metric.CopilotFileReadSize, { + unit: 'By', + advice: { explicitBucketBoundaries: BYTE_BUCKETS }, + }), + } + return cached +} + +// Caps tool.name to the shared catalog (matches Go's cappedToolName): a +// catalog tool keeps its name, everything else (user MCP/custom/unknown) +// collapses to "other" so series count stays finite. +function cappedToolName(name: string): string { + return TOOL_CATALOG[name] ? name : 'other' +} + +// recordSimToolMetric emits copilot.tool.calls (+1) and copilot.tool.duration +// for one server-side Sim tool dispatch (executor=sim). outcome is the bounded +// tool outcome (success/error/…). Pure telemetry. +export function recordSimToolMetric(name: string, outcome: string, durationMs: number): void { + const { toolDuration, toolCalls } = instruments() + const attrs = { + [TraceAttr.ToolName]: cappedToolName(name), + [TraceAttr.ToolExecutor]: 'sim', + [TraceAttr.ToolOutcome]: outcome, + } + toolCalls.add(1, attrs) + if (durationMs >= 0) toolDuration.record(durationMs, attrs) +} + +// recordVfsMaterialize records VFS materialization time. Call once per phase +// with that phase's duration and once with phase="total" for the whole op, so +// the dashboard can show total + per-phase. phase must be a bounded value. +export function recordVfsMaterialize(phase: string, durationMs: number): void { + if (durationMs < 0) return + instruments().vfsMaterializeDuration.record(durationMs, { + [TraceAttr.CopilotVfsPhase]: phase, + }) +} + +// recordFileRead records server-side file-read duration + size by outcome. +export function recordFileRead(outcome: string, durationMs: number, bytes: number): void { + const { fileReadDuration, fileReadBytes } = instruments() + const attrs = { [TraceAttr.CopilotVfsReadOutcome]: outcome } + if (durationMs >= 0) fileReadDuration.record(durationMs, attrs) + if (bytes >= 0) fileReadBytes.record(bytes, attrs) +} diff --git a/apps/sim/lib/copilot/request/tools/executor.ts b/apps/sim/lib/copilot/request/tools/executor.ts index b9a78eae0bf..b302847c7d8 100644 --- a/apps/sim/lib/copilot/request/tools/executor.ts +++ b/apps/sim/lib/copilot/request/tools/executor.ts @@ -43,6 +43,7 @@ import { } from '@/lib/copilot/generated/tool-catalog-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { publishToolConfirmation } from '@/lib/copilot/persistence/tool-confirm' +import { recordSimToolMetric } from '@/lib/copilot/request/metrics' import { withCopilotToolSpan } from '@/lib/copilot/request/otel' import { markToolResultSeen } from '@/lib/copilot/request/sse-utils' import { @@ -397,14 +398,20 @@ export async function executeToolAndReport( argsPreview: argsPayload?.slice(0, 200), }, async (otelSpan) => { + const startedAt = Date.now() const completion = await executeToolAndReportInner(toolCall, context, execContext, options) + const durationMs = Date.now() - startedAt otelSpan.setAttribute(TraceAttr.ToolOutcome, completion.status) + otelSpan.setAttribute(TraceAttr.ToolDurationMs, durationMs) if (completion.message) { otelSpan.setAttribute( TraceAttr.ToolOutcomeMessage, String(completion.message).slice(0, 500) ) } + // Durable Grafana signal for "which Sim tool is slowest" (executor=sim); + // pairs with the Go executor-boundary metric (U15) as one series set. + recordSimToolMetric(toolCall.name, completion.status, durationMs) return completion } ) diff --git a/apps/sim/lib/copilot/vfs/file-reader.ts b/apps/sim/lib/copilot/vfs/file-reader.ts index f2d65252a27..26388d5621a 100644 --- a/apps/sim/lib/copilot/vfs/file-reader.ts +++ b/apps/sim/lib/copilot/vfs/file-reader.ts @@ -9,6 +9,7 @@ import { import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceEvent } from '@/lib/copilot/generated/trace-events-v1' import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1' +import { recordFileRead } from '@/lib/copilot/request/metrics' import { markSpanForError } from '@/lib/copilot/request/otel' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { fetchWorkspaceFileBuffer } from '@/lib/uploads/contexts/workspace/workspace-file-manager' @@ -284,7 +285,8 @@ export interface FileReadResult { * nests underneath for the image-resize path. */ export async function readFileRecord(record: WorkspaceFileRecord): Promise { - return getVfsTracer().startActiveSpan( + const startedAt = Date.now() + const result = await getVfsTracer().startActiveSpan( TraceSpan.CopilotVfsReadFile, { attributes: { @@ -414,4 +416,9 @@ export async function readFileRecord(record: WorkspaceFileRecord): Promise): string[] { + const defs = (schema.$defs ?? {}) as Record + const nameDef = defs.MetricsV1Name + if ( + !nameDef || + typeof nameDef !== 'object' || + !Array.isArray((nameDef as Record).enum) + ) { + throw new Error('metrics-v1.schema.json is missing $defs.MetricsV1Name.enum') + } + const enumValues = (nameDef as Record).enum as unknown[] + if (!enumValues.every((v) => typeof v === 'string')) { + throw new Error('MetricsV1Name enum must be string-only') + } + return (enumValues as string[]).slice().sort() +} + +/** + * Convert a wire metric name like `copilot.request.duration` into an + * identifier-safe PascalCase key like `CopilotRequestDuration`. Same algorithm + * as the trace-attributes sync script so readers learn one and reuse it. + */ +function toIdentifier(name: string): string { + const parts = name.split(/[^A-Za-z0-9]+/).filter(Boolean) + if (parts.length === 0) { + throw new Error(`Cannot derive identifier for metric name: ${name}`) + } + const ident = parts.map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join('') + if (/^[0-9]/.test(ident)) { + throw new Error(`Derived identifier "${ident}" for metric "${name}" starts with a digit`) + } + return ident +} + +function render(metricNames: string[]): string { + const pairs = metricNames.map((name) => ({ name, ident: toIdentifier(name) })) + + const seen = new Map() + for (const p of pairs) { + const prev = seen.get(p.ident) + if (prev && prev !== p.name) { + throw new Error(`Identifier collision: "${prev}" and "${p.name}" both map to "${p.ident}"`) + } + seen.set(p.ident, p.name) + } + + const constLines = pairs.map((p) => ` ${p.ident}: ${JSON.stringify(p.name)},`).join('\n') + const arrayEntries = metricNames.map((n) => ` ${JSON.stringify(n)},`).join('\n') + + return `// AUTO-GENERATED FILE. DO NOT EDIT. +// +// Source: copilot/copilot/contracts/metrics-v1.schema.json +// Regenerate with: bun run metrics-contract:generate +// +// Canonical mothership OTel metric names. Call sites should reference +// \`Metric.\` (e.g. \`Metric.CopilotToolDuration\`) rather than raw +// string literals, so the Go-side contract is the single source of truth and +// typos become compile errors. +// +// NAMES ONLY. Label keys and histogram bucket boundaries are NOT in this +// contract — Go owns the label-cardinality allowlist and the shared bucket +// constant, and the Sim emitter MUST mirror those by hand so the Go∪Sim metric +// union is queryable as one series set. + +export const Metric = { +${constLines} +} as const; + +export type MetricKey = keyof typeof Metric; +export type MetricValue = (typeof Metric)[MetricKey]; + +/** Readonly sorted list of every canonical mothership metric name. */ +export const MetricValues: readonly MetricValue[] = [ +${arrayEntries} +] as const; +` +} + +async function main() { + const checkOnly = process.argv.includes('--check') + const inputArg = process.argv.find((a) => a.startsWith('--input=')) + const inputPath = inputArg + ? resolve(ROOT, inputArg.slice('--input='.length)) + : DEFAULT_CONTRACT_PATH + + const raw = await readFile(inputPath, 'utf8') + const schema = JSON.parse(raw) + const metricNames = extractMetricNames(schema) + const rendered = formatGeneratedSource(render(metricNames), OUTPUT_PATH, ROOT) + + if (checkOnly) { + const existing = await readFile(OUTPUT_PATH, 'utf8').catch(() => null) + if (existing !== rendered) { + throw new Error('Generated metrics contract is stale. Run: bun run metrics-contract:generate') + } + console.log('Metrics contract is up to date.') + return + } + + await mkdir(dirname(OUTPUT_PATH), { recursive: true }) + await writeFile(OUTPUT_PATH, rendered, 'utf8') + console.log(`Generated metrics types -> ${OUTPUT_PATH}`) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) From 6f2c355a7ec0d74d27ea3060ca620f2301fcff4c Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 15 Jun 2026 15:12:54 -0700 Subject: [PATCH 2/4] fix(lint): fix lint --- .../lib/copilot/generated/tool-schemas-v1.ts | 196 +++++++++--------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index a93837798b1..cd6421a0310 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_job']: { + complete_job: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -96,7 +96,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -162,7 +162,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_file_folder']: { + create_file_folder: { parameters: { type: 'object', properties: { @@ -180,7 +180,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_folder']: { + create_folder: { parameters: { type: 'object', properties: { @@ -201,7 +201,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -226,7 +226,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -259,7 +259,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -289,7 +289,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_file_folder']: { + delete_file_folder: { parameters: { type: 'object', properties: { @@ -305,7 +305,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_folder']: { + delete_folder: { parameters: { type: 'object', properties: { @@ -321,7 +321,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -337,7 +337,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -350,7 +350,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -364,7 +364,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -448,7 +448,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -607,7 +607,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -723,7 +723,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['diff_workflows']: { + diff_workflows: { parameters: { type: 'object', properties: { @@ -747,7 +747,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -796,7 +796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -828,7 +828,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -867,7 +867,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['enrichment_run']: { + enrichment_run: { parameters: { type: 'object', properties: { @@ -911,7 +911,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['matched', 'result'], }, }, - ['ffmpeg']: { + ffmpeg: { parameters: { type: 'object', properties: { @@ -1092,7 +1092,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { properties: { prompt: { @@ -1105,7 +1105,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -1243,7 +1243,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -1261,7 +1261,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_audio']: { + generate_audio: { parameters: { type: 'object', properties: { @@ -1413,7 +1413,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -1541,7 +1541,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_video']: { + generate_video: { parameters: { type: 'object', properties: { @@ -1708,7 +1708,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -1729,7 +1729,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1751,7 +1751,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1764,7 +1764,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_log']: { + get_deployment_log: { parameters: { type: 'object', properties: { @@ -1777,7 +1777,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_job_logs']: { + get_job_logs: { parameters: { type: 'object', properties: { @@ -1802,7 +1802,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1830,14 +1830,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1856,7 +1856,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_run_options']: { + get_workflow_run_options: { parameters: { type: 'object', properties: { @@ -1869,7 +1869,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1888,7 +1888,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1936,7 +1936,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['job']: { + job: { parameters: { properties: { request: { @@ -1949,7 +1949,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1962,7 +1962,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -2155,7 +2155,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_file_folders']: { + list_file_folders: { parameters: { type: 'object', properties: { @@ -2167,7 +2167,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_folders']: { + list_folders: { parameters: { type: 'object', properties: { @@ -2179,7 +2179,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_integration_tools']: { + list_integration_tools: { parameters: { properties: { integration: { @@ -2193,14 +2193,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -2213,7 +2213,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['load_deployment']: { + load_deployment: { parameters: { type: 'object', properties: { @@ -2232,7 +2232,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['load_integration_tool']: { + load_integration_tool: { parameters: { properties: { tool_ids: { @@ -2249,7 +2249,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -2278,7 +2278,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -2358,7 +2358,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_job']: { + manage_job: { parameters: { type: 'object', properties: { @@ -2433,7 +2433,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -2485,7 +2485,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -2518,7 +2518,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -2542,7 +2542,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['media']: { + media: { parameters: { properties: { prompt: { @@ -2555,7 +2555,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_file']: { + move_file: { parameters: { type: 'object', properties: { @@ -2576,7 +2576,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_file_folder']: { + move_file_folder: { parameters: { type: 'object', properties: { @@ -2594,7 +2594,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_folder']: { + move_folder: { parameters: { type: 'object', properties: { @@ -2612,7 +2612,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -2632,7 +2632,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -2646,7 +2646,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -2660,7 +2660,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -2694,7 +2694,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['promote_to_live']: { + promote_to_live: { parameters: { type: 'object', properties: { @@ -2713,7 +2713,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['query_logs']: { + query_logs: { parameters: { type: 'object', properties: { @@ -2824,7 +2824,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -2851,7 +2851,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -2930,7 +2930,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2966,7 +2966,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_file_folder']: { + rename_file_folder: { parameters: { type: 'object', properties: { @@ -2983,7 +2983,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -3000,7 +3000,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -3013,7 +3013,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -3036,7 +3036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -3054,7 +3054,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -3071,7 +3071,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -3103,7 +3103,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -3135,7 +3135,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -3173,7 +3173,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -3216,7 +3216,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -3237,7 +3237,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -3254,7 +3254,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -3275,7 +3275,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -3315,7 +3315,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -3337,7 +3337,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -3359,7 +3359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -3393,7 +3393,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -3434,7 +3434,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -3448,7 +3448,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -3461,7 +3461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_deployment_version']: { + update_deployment_version: { parameters: { type: 'object', properties: { @@ -3490,7 +3490,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_job_history']: { + update_job_history: { parameters: { type: 'object', properties: { @@ -3508,7 +3508,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -3533,7 +3533,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -3582,7 +3582,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -3944,7 +3944,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { properties: { prompt: { @@ -3957,7 +3957,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: { From b27e4c2a9587ab3ec11391ed9ca7edb6885d42ae Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 15 Jun 2026 15:40:13 -0700 Subject: [PATCH 3/4] fix(vfs): add error on vfs materialize fail --- .../sim/lib/copilot/request/tools/executor.ts | 37 ++++++++++++------- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 17 ++++++--- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/apps/sim/lib/copilot/request/tools/executor.ts b/apps/sim/lib/copilot/request/tools/executor.ts index b302847c7d8..b1637b017ce 100644 --- a/apps/sim/lib/copilot/request/tools/executor.ts +++ b/apps/sim/lib/copilot/request/tools/executor.ts @@ -399,20 +399,31 @@ export async function executeToolAndReport( }, async (otelSpan) => { const startedAt = Date.now() - const completion = await executeToolAndReportInner(toolCall, context, execContext, options) - const durationMs = Date.now() - startedAt - otelSpan.setAttribute(TraceAttr.ToolOutcome, completion.status) - otelSpan.setAttribute(TraceAttr.ToolDurationMs, durationMs) - if (completion.message) { - otelSpan.setAttribute( - TraceAttr.ToolOutcomeMessage, - String(completion.message).slice(0, 500) - ) + try { + const completion = await executeToolAndReportInner(toolCall, context, execContext, options) + const durationMs = Date.now() - startedAt + otelSpan.setAttribute(TraceAttr.ToolOutcome, completion.status) + otelSpan.setAttribute(TraceAttr.ToolDurationMs, durationMs) + if (completion.message) { + otelSpan.setAttribute( + TraceAttr.ToolOutcomeMessage, + String(completion.message).slice(0, 500) + ) + } + // Durable Grafana signal for "which Sim tool is slowest" (executor=sim); + // pairs with the Go executor-boundary metric (U15) as one series set. + recordSimToolMetric(toolCall.name, completion.status, durationMs) + return completion + } catch (err) { + // executeToolAndReportInner threw (infra/unexpected error, not a normal + // 'error' completion). Still stamp the span + record the dispatch so + // copilot.tool.* isn't silently biased toward successful calls. + const durationMs = Date.now() - startedAt + otelSpan.setAttribute(TraceAttr.ToolOutcome, 'error') + otelSpan.setAttribute(TraceAttr.ToolDurationMs, durationMs) + recordSimToolMetric(toolCall.name, 'error', durationMs) + throw err } - // Durable Grafana signal for "which Sim tool is slowest" (executor=sim); - // pairs with the Go executor-boundary metric (U15) as one series set. - recordSimToolMetric(toolCall.name, completion.status, durationMs) - return completion } ) } diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 4fc7ad2c39d..29b20ab5a8f 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -473,20 +473,25 @@ export class WorkspaceVFS { markSpanForError(span, err) throw err } finally { + // Record on success AND failure: a mid-phase failure (e.g. a DB + // timeout) still belongs in copilot.vfs.materialize.duration, else + // p50/p99 skew toward successes only. phaseMs holds whatever phases + // completed before the failure. + for (const [phase, ms] of Object.entries(phaseMs)) { + recordVfsMaterialize(phase, ms) + } + recordVfsMaterialize('total', Date.now() - start) span.end() } } ) - const totalMs = Date.now() - start // Durable Grafana signal for "how long does VFS materialize" — total plus // per-phase (bounded phase set). getOrMaterializeVFS runs per VFS tool call // with no cross-request cache, so this reveals whether materialize is the - // bottleneck (observability only; not a fix). - for (const [phase, ms] of Object.entries(phaseMs)) { - recordVfsMaterialize(phase, ms) - } - recordVfsMaterialize('total', totalMs) + // bottleneck (observability only; not a fix). Recorded inside the span's + // finally above so a failed materialize is captured too, not just successes. + const totalMs = Date.now() - start logger.info('VFS materialized', { workspaceId, From 7803f47eeff055cf3bc4053957ebf902fad83533 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 15 Jun 2026 16:18:50 -0700 Subject: [PATCH 4/4] improvement(mship): add user metadata to mship request --- apps/sim/lib/copilot/chat/payload.ts | 4 +++- apps/sim/lib/copilot/chat/post.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index 31da4396bc1..6c5e881e54f 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -37,6 +37,7 @@ interface BuildPayloadParams { userTimezone?: string userMetadata?: { name?: string + email?: string timezone?: string } includeMothershipTools?: boolean @@ -367,7 +368,8 @@ export async function buildCopilotRequestPayload( ...(params.workspaceContext ? { workspaceContext: params.workspaceContext } : {}), ...(params.userPermission ? { userPermission: params.userPermission } : {}), ...(params.userTimezone ? { userTimezone: params.userTimezone } : {}), - ...(params.userMetadata && (params.userMetadata.name || params.userMetadata.timezone) + ...(params.userMetadata && + (params.userMetadata.name || params.userMetadata.email || params.userMetadata.timezone) ? { userMetadata: params.userMetadata } : {}), // Tell the copilot file subagent which document toolchain to write. Emitted diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index 5465b25437a..721aa8b5519 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -169,7 +169,7 @@ type UnifiedChatBranch = fileAttachments?: UnifiedChatRequest['fileAttachments'] userPermission?: string userTimezone?: string - userMetadata?: { name?: string; timezone?: string } + userMetadata?: { name?: string; email?: string; timezone?: string } workflowId: string workflowName?: string workspaceId?: string @@ -204,7 +204,7 @@ type UnifiedChatBranch = fileAttachments?: UnifiedChatRequest['fileAttachments'] userPermission?: string userTimezone?: string - userMetadata?: { name?: string; timezone?: string } + userMetadata?: { name?: string; email?: string; timezone?: string } workspaceContext?: string }) => Promise> buildExecutionContext: (params: { @@ -722,6 +722,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { const body = ChatMessageSchema.parse(await req.json()) const userMetadata = { ...(authenticatedUserName ? { name: authenticatedUserName } : {}), + ...(authenticatedUserEmail ? { email: authenticatedUserEmail } : {}), ...(body.userTimezone ? { timezone: body.userTimezone } : {}), } const normalizedContexts = normalizeContexts(body.contexts) ?? []