Skip to content

Commit ddf9a5d

Browse files
jesseturner21claude
andcommitted
refactor: extract shared agent resolution + add --since/--until to traces list
Extract duplicate agent resolution logic into a shared resolve-agent utility (src/cli/operations/resolve-agent.ts) used by both logs and traces commands. Move time-parser to shared utils (src/lib/utils/time-parser.ts). Add --since and --until flags to `traces list` for custom time range queries instead of the hardcoded 12h window. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5b44260 commit ddf9a5d

16 files changed

Lines changed: 205 additions & 218 deletions

File tree

src/cli/commands/logs/__tests__/action.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { detectMode, formatLogLine, resolveAgentContext } from '../action';
2-
import type { LogsContext } from '../action';
2+
import type { DeployedProjectConfig } from '../action';
33
import { describe, expect, it } from 'vitest';
44

55
describe('detectMode', () => {
@@ -39,7 +39,7 @@ describe('formatLogLine', () => {
3939

4040
describe('resolveAgentContext', () => {
4141
// Use 'as any' to avoid branded type issues with FilePath/DirectoryPath
42-
const makeContext = (overrides?: Partial<LogsContext>): LogsContext => ({
42+
const makeContext = (overrides?: Partial<DeployedProjectConfig>): DeployedProjectConfig => ({
4343
project: {
4444
name: 'TestProject',
4545
version: 1,

src/cli/commands/logs/__tests__/time-parser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseTimeString } from '../time-parser';
1+
import { parseTimeString } from '../../../../lib/utils/time-parser';
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
33

44
describe('parseTimeString', () => {

src/cli/commands/logs/action.ts

Lines changed: 15 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import { ConfigIO } from '../../../lib';
2-
import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedState } from '../../../schema';
1+
import { parseTimeString } from '../../../lib/utils';
32
import { searchLogs, streamLogs } from '../../aws/cloudwatch';
3+
import type { DeployedProjectConfig } from '../../operations/resolve-agent';
4+
import { loadDeployedProjectConfig, resolveAgent } from '../../operations/resolve-agent';
45
import { VALID_LEVELS, buildFilterPattern } from './filter-pattern';
5-
import { parseTimeString } from './time-parser';
66
import type { LogsOptions } from './types';
77

8-
export interface LogsContext {
9-
project: AgentCoreProjectSpec;
10-
deployedState: DeployedState;
11-
awsTargets: AwsDeploymentTargets;
12-
}
8+
export type { DeployedProjectConfig };
139

1410
export interface AgentContext {
1511
agentId: string;
@@ -25,17 +21,6 @@ export interface LogsResult {
2521
error?: string;
2622
}
2723

28-
/**
29-
* Loads configuration required for logs
30-
*/
31-
export async function loadLogsConfig(configIO: ConfigIO = new ConfigIO()): Promise<LogsContext> {
32-
return {
33-
project: await configIO.readProjectSpec(),
34-
deployedState: await configIO.readDeployedState(),
35-
awsTargets: await configIO.readAWSDeploymentTargets(),
36-
};
37-
}
38-
3924
/**
4025
* Detect whether to stream or search based on options
4126
*/
@@ -61,73 +46,23 @@ export function formatLogLine(event: { timestamp: number; message: string }, jso
6146
* Resolve agent context from config + options
6247
*/
6348
export function resolveAgentContext(
64-
context: LogsContext,
49+
context: DeployedProjectConfig,
6550
options: LogsOptions
6651
): { success: true; agentContext: AgentContext } | { success: false; error: string } {
67-
const { project, deployedState, awsTargets } = context;
68-
69-
if (project.agents.length === 0) {
70-
return { success: false, error: 'No agents defined in agentcore.json' };
71-
}
72-
73-
// Resolve agent
74-
const agentNames = project.agents.map(a => a.name);
75-
76-
if (!options.agent && project.agents.length > 1) {
77-
return {
78-
success: false,
79-
error: `Multiple agents found. Use --agent to specify one: ${agentNames.join(', ')}`,
80-
};
52+
const result = resolveAgent(context, options);
53+
if (!result.success) {
54+
return { success: false, error: result.error };
8155
}
82-
83-
const agentSpec = options.agent ? project.agents.find(a => a.name === options.agent) : project.agents[0];
84-
85-
if (options.agent && !agentSpec) {
86-
return {
87-
success: false,
88-
error: `Agent '${options.agent}' not found. Available: ${agentNames.join(', ')}`,
89-
};
90-
}
91-
92-
if (!agentSpec) {
93-
return { success: false, error: 'No agents defined in agentcore.json' };
94-
}
95-
96-
// Resolve target
97-
const targetNames = Object.keys(deployedState.targets);
98-
if (targetNames.length === 0) {
99-
return { success: false, error: 'No deployed targets found. Run `agentcore deploy` first.' };
100-
}
101-
const selectedTargetName = targetNames[0]!;
102-
103-
const targetState = deployedState.targets[selectedTargetName];
104-
const targetConfig = awsTargets.find(t => t.name === selectedTargetName);
105-
106-
if (!targetConfig) {
107-
return { success: false, error: `Target config '${selectedTargetName}' not found in aws-targets` };
108-
}
109-
110-
// Get the deployed state for this specific agent
111-
const agentState = targetState?.resources?.agents?.[agentSpec.name];
112-
113-
if (!agentState) {
114-
return {
115-
success: false,
116-
error: `Agent '${agentSpec.name}' is not deployed to target '${selectedTargetName}'. Run 'agentcore deploy' first.`,
117-
};
118-
}
119-
120-
const agentId = agentState.runtimeId;
56+
const { agent } = result;
12157
const endpointName = 'DEFAULT';
122-
const logGroupName = `/aws/bedrock-agentcore/runtimes/${agentId}-${endpointName}`;
123-
58+
const logGroupName = `/aws/bedrock-agentcore/runtimes/${agent.runtimeId}-${endpointName}`;
12459
return {
12560
success: true,
12661
agentContext: {
127-
agentId,
128-
agentName: agentSpec.name,
129-
accountId: targetConfig.account,
130-
region: targetConfig.region,
62+
agentId: agent.runtimeId,
63+
agentName: agent.agentName,
64+
accountId: agent.accountId,
65+
region: agent.region,
13166
endpointName,
13267
logGroupName,
13368
},
@@ -146,7 +81,7 @@ export async function handleLogs(options: LogsOptions): Promise<LogsResult> {
14681
};
14782
}
14883

149-
const context = await loadLogsConfig();
84+
const context = await loadDeployedProjectConfig();
15085
const resolution = resolveAgentContext(context, options);
15186

15287
if (!resolution.success) {

src/cli/commands/traces/action.ts

Lines changed: 58 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,9 @@
1-
import { ConfigIO } from '../../../lib';
2-
import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedState } from '../../../schema';
3-
import { buildTraceConsoleUrl, getTrace, listTraces, parseRuntimeArn } from '../../operations/traces';
1+
import { parseTimeString } from '../../../lib/utils';
2+
import type { DeployedProjectConfig } from '../../operations/resolve-agent';
3+
import { resolveAgent } from '../../operations/resolve-agent';
4+
import { buildTraceConsoleUrl, getTrace, listTraces } from '../../operations/traces';
45
import type { TracesGetOptions, TracesListOptions } from './types';
56

6-
export interface TracesContext {
7-
project: AgentCoreProjectSpec;
8-
deployedState: DeployedState;
9-
awsTargets: AwsDeploymentTargets;
10-
}
11-
12-
export async function loadTracesConfig(configIO: ConfigIO = new ConfigIO()): Promise<TracesContext> {
13-
return {
14-
project: await configIO.readProjectSpec(),
15-
deployedState: await configIO.readDeployedState(),
16-
awsTargets: await configIO.readAWSDeploymentTargets(),
17-
};
18-
}
19-
20-
interface ResolvedAgent {
21-
agentName: string;
22-
region: string;
23-
accountId: string;
24-
runtimeId: string;
25-
targetName: string;
26-
}
27-
28-
function resolveAgent(context: TracesContext, options: { agent?: string }): ResolvedAgent | { error: string } {
29-
const { project, deployedState, awsTargets } = context;
30-
31-
const targetNames = Object.keys(deployedState.targets);
32-
if (targetNames.length === 0) {
33-
return { error: 'No deployed targets found. Run `agentcore deploy` first.' };
34-
}
35-
36-
const selectedTargetName = targetNames[0]!;
37-
const targetState = deployedState.targets[selectedTargetName];
38-
const targetConfig = awsTargets.find(t => t.name === selectedTargetName);
39-
if (!targetConfig) {
40-
return { error: `Target config '${selectedTargetName}' not found in aws-targets` };
41-
}
42-
43-
if (project.agents.length === 0) {
44-
return { error: 'No agents defined in configuration' };
45-
}
46-
47-
const agentNames = project.agents.map(a => a.name);
48-
if (!options.agent && project.agents.length > 1) {
49-
return { error: `Multiple agents found. Use --agent to specify one: ${agentNames.join(', ')}` };
50-
}
51-
52-
const agentSpec = options.agent ? project.agents.find(a => a.name === options.agent) : project.agents[0];
53-
if (options.agent && !agentSpec) {
54-
return { error: `Agent '${options.agent}' not found. Available: ${agentNames.join(', ')}` };
55-
}
56-
if (!agentSpec) {
57-
return { error: 'No agents defined in configuration' };
58-
}
59-
60-
const agentState = targetState?.resources?.agents?.[agentSpec.name];
61-
if (!agentState) {
62-
return { error: `Agent '${agentSpec.name}' is not deployed to target '${selectedTargetName}'` };
63-
}
64-
65-
const parsed = parseRuntimeArn(agentState.runtimeArn);
66-
if (!parsed) {
67-
return { error: `Could not parse runtime ARN: ${agentState.runtimeArn}` };
68-
}
69-
70-
return {
71-
agentName: agentSpec.name,
72-
region: targetConfig.region,
73-
accountId: parsed.accountId,
74-
runtimeId: parsed.runtimeId,
75-
targetName: selectedTargetName,
76-
};
77-
}
78-
797
export interface TracesListResult {
808
success: boolean;
819
agentName?: string;
@@ -85,25 +13,43 @@ export interface TracesListResult {
8513
error?: string;
8614
}
8715

88-
export async function handleTracesList(context: TracesContext, options: TracesListOptions): Promise<TracesListResult> {
16+
export async function handleTracesList(
17+
context: DeployedProjectConfig,
18+
options: TracesListOptions
19+
): Promise<TracesListResult> {
8920
const resolved = resolveAgent(context, options);
90-
if ('error' in resolved) {
21+
if (!resolved.success) {
9122
return { success: false, error: resolved.error };
9223
}
9324

25+
const { agent } = resolved;
26+
9427
const consoleUrl = buildTraceConsoleUrl({
95-
region: resolved.region,
96-
accountId: resolved.accountId,
97-
runtimeId: resolved.runtimeId,
98-
agentName: resolved.agentName,
28+
region: agent.region,
29+
accountId: agent.accountId,
30+
runtimeId: agent.runtimeId,
31+
agentName: agent.agentName,
9932
});
10033

10134
const limit = options.limit ? parseInt(options.limit, 10) : 20;
35+
36+
// Parse time options
37+
let startTime: number | undefined;
38+
let endTime: number | undefined;
39+
if (options.since) {
40+
startTime = parseTimeString(options.since);
41+
}
42+
if (options.until) {
43+
endTime = parseTimeString(options.until);
44+
}
45+
10246
const result = await listTraces({
103-
region: resolved.region,
104-
runtimeId: resolved.runtimeId,
105-
agentName: resolved.agentName,
47+
region: agent.region,
48+
runtimeId: agent.runtimeId,
49+
agentName: agent.agentName,
10650
limit,
51+
startTime,
52+
endTime,
10753
});
10854

10955
if (!result.success) {
@@ -112,8 +58,8 @@ export async function handleTracesList(context: TracesContext, options: TracesLi
11258

11359
return {
11460
success: true,
115-
agentName: resolved.agentName,
116-
targetName: resolved.targetName,
61+
agentName: agent.agentName,
62+
targetName: agent.targetName,
11763
consoleUrl,
11864
traces: result.traces,
11965
};
@@ -129,28 +75,42 @@ export interface TracesGetResult {
12975
}
13076

13177
export async function handleTracesGet(
132-
context: TracesContext,
78+
context: DeployedProjectConfig,
13379
traceId: string,
13480
options: TracesGetOptions
13581
): Promise<TracesGetResult> {
13682
const resolved = resolveAgent(context, options);
137-
if ('error' in resolved) {
83+
if (!resolved.success) {
13884
return { success: false, error: resolved.error };
13985
}
14086

87+
const { agent } = resolved;
88+
14189
const consoleUrl = buildTraceConsoleUrl({
142-
region: resolved.region,
143-
accountId: resolved.accountId,
144-
runtimeId: resolved.runtimeId,
145-
agentName: resolved.agentName,
90+
region: agent.region,
91+
accountId: agent.accountId,
92+
runtimeId: agent.runtimeId,
93+
agentName: agent.agentName,
14694
});
14795

96+
// Parse time options
97+
let startTime: number | undefined;
98+
let endTime: number | undefined;
99+
if (options.since) {
100+
startTime = parseTimeString(options.since);
101+
}
102+
if (options.until) {
103+
endTime = parseTimeString(options.until);
104+
}
105+
148106
const result = await getTrace({
149-
region: resolved.region,
150-
runtimeId: resolved.runtimeId,
151-
agentName: resolved.agentName,
107+
region: agent.region,
108+
runtimeId: agent.runtimeId,
109+
agentName: agent.agentName,
152110
traceId,
153111
outputPath: options.output,
112+
startTime,
113+
endTime,
154114
});
155115

156116
if (!result.success) {
@@ -159,8 +119,8 @@ export async function handleTracesGet(
159119

160120
return {
161121
success: true,
162-
agentName: resolved.agentName,
163-
targetName: resolved.targetName,
122+
agentName: agent.agentName,
123+
targetName: agent.targetName,
164124
consoleUrl,
165125
filePath: result.filePath,
166126
};

0 commit comments

Comments
 (0)