Skip to content

Commit e753053

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 e753053

12 files changed

Lines changed: 180 additions & 188 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 { AgentResolutionContext } 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<AgentResolutionContext>): AgentResolutionContext => ({
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 { AgentResolutionContext } from '../../operations/resolve-agent';
4+
import { loadAgentResolutionContext, 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 { AgentResolutionContext };
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: AgentResolutionContext,
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 loadAgentResolutionContext();
15085
const resolution = resolveAgentContext(context, options);
15186

15287
if (!resolution.success) {

src/cli/commands/traces/action.ts

Lines changed: 46 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 { AgentResolutionContext } 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: AgentResolutionContext,
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,26 +75,28 @@ export interface TracesGetResult {
12975
}
13076

13177
export async function handleTracesGet(
132-
context: TracesContext,
78+
context: AgentResolutionContext,
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

14896
const result = await getTrace({
149-
region: resolved.region,
150-
runtimeId: resolved.runtimeId,
151-
agentName: resolved.agentName,
97+
region: agent.region,
98+
runtimeId: agent.runtimeId,
99+
agentName: agent.agentName,
152100
traceId,
153101
outputPath: options.output,
154102
});
@@ -159,8 +107,8 @@ export async function handleTracesGet(
159107

160108
return {
161109
success: true,
162-
agentName: resolved.agentName,
163-
targetName: resolved.targetName,
110+
agentName: agent.agentName,
111+
targetName: agent.targetName,
164112
consoleUrl,
165113
filePath: result.filePath,
166114
};

src/cli/commands/traces/command.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { getErrorMessage } from '../../errors';
2+
import { loadAgentResolutionContext } from '../../operations/resolve-agent';
23
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
34
import { requireProject } from '../../tui/guards';
4-
import { handleTracesGet, handleTracesList, loadTracesConfig } from './action';
5+
import { handleTracesGet, handleTracesList } from './action';
56
import type { TracesGetOptions, TracesListOptions } from './types';
67
import type { Command } from '@commander-js/extra-typings';
78
import { Box, Text, render } from 'ink';
@@ -24,11 +25,13 @@ export const registerTraces = (program: Command) => {
2425
.description('List recent traces for a deployed agent')
2526
.option('--agent <name>', 'Select specific agent')
2627
.option('--limit <n>', 'Maximum number of traces to display', '20')
28+
.option('--since <time>', 'Start time (e.g. 5m, 1h, 2d, ISO 8601, epoch ms)')
29+
.option('--until <time>', 'End time (e.g. now, 1h, ISO 8601, epoch ms)')
2730
.action(async (cliOptions: TracesListOptions) => {
2831
requireProject();
2932

3033
try {
31-
const context = await loadTracesConfig();
34+
const context = await loadAgentResolutionContext();
3235
const result = await handleTracesList(context, cliOptions);
3336

3437
if (!result.success) {
@@ -76,7 +79,7 @@ export const registerTraces = (program: Command) => {
7679
))}
7780
</>
7881
) : (
79-
<Text color="yellow">No traces found in the last 12 hours.</Text>
82+
<Text color="yellow">No traces found in the specified time range.</Text>
8083
)}
8184
<Text> </Text>
8285
{result.consoleUrl && <Text color="gray">Console: {result.consoleUrl}</Text>}
@@ -97,7 +100,7 @@ export const registerTraces = (program: Command) => {
97100
requireProject();
98101

99102
try {
100-
const context = await loadTracesConfig();
103+
const context = await loadAgentResolutionContext();
101104
const result = await handleTracesGet(context, traceId, cliOptions);
102105

103106
if (!result.success) {

src/cli/commands/traces/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export { registerTraces } from './command';
2-
export { handleTracesList, handleTracesGet, loadTracesConfig, type TracesContext } from './action';
2+
export { handleTracesList, handleTracesGet } from './action';

0 commit comments

Comments
 (0)