@hatchway/cli 0.50.63 → 0.50.65

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/dist/index.js CHANGED
@@ -4335,9 +4335,10 @@ function buildPromptWithImages(prompt, messageParts) {
4335
4335
  * query() SDK function -> minimal transformation -> output
4336
4336
  *
4337
4337
  * Sentry Integration:
4338
- * - The query() function is auto-instrumented by Sentry's claudeCodeAgentSdkIntegration
4339
- * - Instrumentation hooks into @anthropic-ai/claude-agent-sdk via OpenTelemetry
4340
- * - IMPORTANT: Sentry must be initialized BEFORE claude-agent-sdk is imported
4338
+ * - Manual gen_ai.* spans for AI Agent Monitoring in Sentry
4339
+ * - gen_ai.invoke_agent wraps the full query lifecycle
4340
+ * - gen_ai.execute_tool spans are emitted per tool call
4341
+ * - Token usage and cost are captured from the SDK result message
4341
4342
  */
4342
4343
  function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortController) {
4343
4344
  return async function* nativeClaudeQuery(prompt, workingDirectory, systemPrompt, _agent, _codexThreadId, messageParts) {
@@ -4423,20 +4424,62 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4423
4424
  let messageCount = 0;
4424
4425
  let toolCallCount = 0;
4425
4426
  let textBlockCount = 0;
4427
+ // Create the gen_ai.invoke_agent span using startSpanManual.
4428
+ //
4429
+ // Why startSpanManual and not startSpan?
4430
+ // startSpan() takes a callback and ends the span when the callback returns.
4431
+ // But this is an async generator — we can't yield from inside a callback.
4432
+ // startSpanManual() makes the span active on the current scope AND gives us
4433
+ // a handle to end it ourselves in the finally block.
4434
+ //
4435
+ // Why this works now (it didn't before):
4436
+ // engine.ts captures the parent build.runner span before creating the
4437
+ // ReadableStream, then restores it via Sentry.withActiveSpan() inside the
4438
+ // stream's start() callback. So when this generator runs, the build.runner
4439
+ // span is the active parent, and our gen_ai.invoke_agent becomes its child.
4440
+ // Tool spans created with startSpan() inside the loop become children of
4441
+ // gen_ai.invoke_agent because it's the active span at that point.
4442
+ const agentSpan = Sentry.startSpanManual({
4443
+ op: 'gen_ai.invoke_agent',
4444
+ name: `Claude Agent (${modelId})`,
4445
+ attributes: {
4446
+ 'gen_ai.agent.name': 'hatchway-builder',
4447
+ 'gen_ai.request.model': modelId,
4448
+ 'gen_ai.agent.input': finalPrompt.substring(0, 500),
4449
+ 'gen_ai.system_prompt.length': appendedSystemPrompt.length,
4450
+ 'gen_ai.agent.available_tools': JSON.stringify(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'Task', 'TodoWrite', 'WebFetch']),
4451
+ },
4452
+ }, (span) => span // Return the span so we control its lifecycle
4453
+ );
4426
4454
  try {
4427
4455
  // Stream messages directly from the SDK
4428
- // NOTE: query() is auto-instrumented by Sentry's claudeCodeAgentSdkIntegration
4429
4456
  for await (const sdkMessage of query({ prompt: finalPrompt, options })) {
4430
4457
  messageCount++;
4431
4458
  // Transform SDK message to our internal format
4432
4459
  const transformed = transformSDKMessage(sdkMessage);
4433
4460
  if (transformed) {
4434
- // Track stats for logging
4461
+ // Track stats and emit gen_ai.execute_tool spans for tool calls
4435
4462
  if (transformed.type === 'assistant' && transformed.message?.content) {
4436
4463
  for (const block of transformed.message.content) {
4437
4464
  if (block.type === 'tool_use') {
4438
4465
  toolCallCount++;
4439
4466
  debugLog$4(`[runner] [native-sdk] 🔧 Tool call: ${block.name}\n`);
4467
+ // Emit a gen_ai.execute_tool span as a child of gen_ai.invoke_agent.
4468
+ // Using startSpan (active) with an empty callback — the span is created,
4469
+ // becomes briefly active, records the tool invocation, and ends when
4470
+ // the callback returns. This gives Sentry the tool call event with
4471
+ // proper parent-child nesting.
4472
+ Sentry.startSpan({
4473
+ op: 'gen_ai.execute_tool',
4474
+ name: `Tool: ${block.name}`,
4475
+ attributes: {
4476
+ 'gen_ai.tool.name': block.name,
4477
+ 'gen_ai.tool.call_id': block.id,
4478
+ 'gen_ai.tool.input': JSON.stringify(block.input).substring(0, 1000),
4479
+ },
4480
+ }, () => {
4481
+ // Span created and ended — marks the tool invocation point
4482
+ });
4440
4483
  }
4441
4484
  else if (block.type === 'text') {
4442
4485
  textBlockCount++;
@@ -4485,13 +4528,31 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4485
4528
  process.stderr.write(`[native-sdk] Tool use summary: ${summaryMsg.summary}\n`);
4486
4529
  }
4487
4530
  }
4488
- // Log result messages
4531
+ // Capture result messages — record token usage and cost on the agent span
4489
4532
  if (sdkMessage.type === 'result') {
4490
- if (sdkMessage.subtype === 'success') {
4491
- debugLog$4(`[runner] [native-sdk] ✅ Query complete - ${sdkMessage.num_turns} turns, $${sdkMessage.total_cost_usd?.toFixed(4)} USD\n`);
4533
+ const resultMsg = sdkMessage;
4534
+ if (agentSpan) {
4535
+ agentSpan.setAttribute('gen_ai.usage.input_tokens', resultMsg.usage?.input_tokens ?? 0);
4536
+ agentSpan.setAttribute('gen_ai.usage.output_tokens', resultMsg.usage?.output_tokens ?? 0);
4537
+ agentSpan.setAttribute('gen_ai.usage.total_tokens', (resultMsg.usage?.input_tokens ?? 0) + (resultMsg.usage?.output_tokens ?? 0));
4538
+ agentSpan.setAttribute('gen_ai.usage.cost_usd', resultMsg.total_cost_usd ?? 0);
4539
+ agentSpan.setAttribute('gen_ai.agent.num_turns', resultMsg.num_turns ?? 0);
4540
+ agentSpan.setAttribute('gen_ai.agent.num_tool_calls', toolCallCount);
4541
+ agentSpan.setAttribute('gen_ai.agent.result', resultMsg.subtype ?? 'unknown');
4542
+ agentSpan.setAttribute('gen_ai.agent.duration_ms', resultMsg.duration_ms ?? 0);
4543
+ agentSpan.setAttribute('gen_ai.agent.duration_api_ms', resultMsg.duration_api_ms ?? 0);
4544
+ if (resultMsg.usage?.cache_read_input_tokens) {
4545
+ agentSpan.setAttribute('gen_ai.usage.cache_read_tokens', resultMsg.usage.cache_read_input_tokens);
4546
+ }
4547
+ if (resultMsg.usage?.cache_creation_input_tokens) {
4548
+ agentSpan.setAttribute('gen_ai.usage.cache_creation_tokens', resultMsg.usage.cache_creation_input_tokens);
4549
+ }
4550
+ }
4551
+ if (resultMsg.subtype === 'success') {
4552
+ debugLog$4(`[runner] [native-sdk] ✅ Query complete - ${resultMsg.num_turns} turns, $${resultMsg.total_cost_usd?.toFixed(4)} USD\n`);
4492
4553
  }
4493
4554
  else {
4494
- debugLog$4(`[runner] [native-sdk] ⚠️ Query ended with: ${sdkMessage.subtype}\n`);
4555
+ debugLog$4(`[runner] [native-sdk] ⚠️ Query ended with: ${resultMsg.subtype}\n`);
4495
4556
  }
4496
4557
  }
4497
4558
  }
@@ -4499,9 +4560,16 @@ function createNativeClaudeQuery(modelId = DEFAULT_CLAUDE_MODEL_ID, abortControl
4499
4560
  }
4500
4561
  catch (error) {
4501
4562
  debugLog$4(`[runner] [native-sdk] ❌ Error: ${error instanceof Error ? error.message : String(error)}\n`);
4563
+ if (agentSpan) {
4564
+ agentSpan.setStatus({ code: 2, message: error instanceof Error ? error.message : String(error) });
4565
+ }
4502
4566
  Sentry.captureException(error);
4503
4567
  throw error;
4504
4568
  }
4569
+ finally {
4570
+ // End the agent span regardless of success/failure
4571
+ agentSpan?.end();
4572
+ }
4505
4573
  };
4506
4574
  }
4507
4575
 
@@ -7127,38 +7195,53 @@ async function createBuildStream(options) {
7127
7195
  debugLog$1();
7128
7196
  const generator = query(fullPrompt, actualWorkingDir, systemPrompt, agent, options.codexThreadId, messageParts);
7129
7197
  debugLog$1();
7198
+ // Capture the active Sentry span BEFORE creating the ReadableStream.
7199
+ // The ReadableStream.start() callback runs in a new async context where the
7200
+ // parent build.runner span is no longer active. We restore it with withActiveSpan()
7201
+ // so that gen_ai.invoke_agent spans created inside the query generator are
7202
+ // properly nested as children of the build.runner span.
7203
+ const parentSpan = Sentry.getActiveSpan();
7130
7204
  // Create a ReadableStream from the AsyncGenerator
7131
7205
  const stream = new ReadableStream({
7132
7206
  async start(controller) {
7133
7207
  debugLog$1();
7134
- let chunkCount = 0;
7135
- try {
7136
- for await (const chunk of generator) {
7137
- chunkCount++;
7138
- if (chunkCount % 5 === 0) {
7139
- debugLog$1(`[runner] [build-engine] Processed ${chunkCount} chunks from generator\n`);
7140
- }
7141
- // Convert chunk to appropriate format
7142
- if (typeof chunk === 'string') {
7143
- controller.enqueue(new TextEncoder().encode(chunk));
7144
- }
7145
- else if (chunk instanceof Uint8Array) {
7146
- controller.enqueue(chunk);
7147
- }
7148
- else if (typeof chunk === 'object') {
7149
- controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk)));
7208
+ const consume = async () => {
7209
+ let chunkCount = 0;
7210
+ try {
7211
+ for await (const chunk of generator) {
7212
+ chunkCount++;
7213
+ if (chunkCount % 5 === 0) {
7214
+ debugLog$1(`[runner] [build-engine] Processed ${chunkCount} chunks from generator\n`);
7215
+ }
7216
+ // Convert chunk to appropriate format
7217
+ if (typeof chunk === 'string') {
7218
+ controller.enqueue(new TextEncoder().encode(chunk));
7219
+ }
7220
+ else if (chunk instanceof Uint8Array) {
7221
+ controller.enqueue(chunk);
7222
+ }
7223
+ else if (typeof chunk === 'object') {
7224
+ controller.enqueue(new TextEncoder().encode(JSON.stringify(chunk)));
7225
+ }
7150
7226
  }
7227
+ debugLog$1(`[runner] [build-engine] ✅ Generator exhausted after ${chunkCount} chunks, closing stream\n`);
7228
+ controller.close();
7151
7229
  }
7152
- debugLog$1(`[runner] [build-engine] ✅ Generator exhausted after ${chunkCount} chunks, closing stream\n`);
7153
- controller.close();
7154
- }
7155
- catch (error) {
7156
- debugLog$1();
7157
- controller.error(error);
7230
+ catch (error) {
7231
+ debugLog$1();
7232
+ controller.error(error);
7233
+ }
7234
+ finally {
7235
+ // Restore the original working directory
7236
+ process.chdir(originalCwd);
7237
+ }
7238
+ };
7239
+ // Restore the parent span context so child spans nest correctly
7240
+ if (parentSpan) {
7241
+ await Sentry.withActiveSpan(parentSpan, consume);
7158
7242
  }
7159
- finally {
7160
- // Restore the original working directory
7161
- process.chdir(originalCwd);
7243
+ else {
7244
+ await consume();
7162
7245
  }
7163
7246
  },
7164
7247
  });