@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 +117 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
* -
|
|
4339
|
-
* -
|
|
4340
|
-
* -
|
|
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
|
|
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
|
-
//
|
|
4531
|
+
// Capture result messages — record token usage and cost on the agent span
|
|
4489
4532
|
if (sdkMessage.type === 'result') {
|
|
4490
|
-
|
|
4491
|
-
|
|
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: ${
|
|
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
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
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
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
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
|
-
|
|
7160
|
-
|
|
7161
|
-
process.chdir(originalCwd);
|
|
7243
|
+
else {
|
|
7244
|
+
await consume();
|
|
7162
7245
|
}
|
|
7163
7246
|
},
|
|
7164
7247
|
});
|