Skip to content

Commit 0aa9d55

Browse files
authored
feat(status): display runtime invocation URL for deployed agents (#775)
Show the runtime invocation URL in agentcore status output for each deployed agent. The URL is computed from the runtime ARN and target region, and displayed in CLI text output, JSON output, and the TUI ResourceGraph component. URL format: https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encodedArn}/invocations Closes #716 Constraint: URL is only available when both targetConfig and runtimeArn exist Rejected: Reuse existing buildInvokeUrl from agentcore.ts | includes ?qualifier=DEFAULT which is for API invocation, not display Confidence: high Scope-risk: narrow
1 parent a6bf024 commit 0aa9d55

File tree

5 files changed

+203
-3
lines changed

5 files changed

+203
-3
lines changed

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

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AgentCoreProjectSpec, DeployedResourceState } from '../../../../schema/index.js';
22
import { computeResourceStatuses, handleProjectStatus } from '../action.js';
33
import type { StatusContext } from '../action.js';
4+
import { buildRuntimeInvocationUrl } from '../constants.js';
45
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
56

67
const mockGetAgentRuntimeStatus = vi.fn();
@@ -631,3 +632,176 @@ describe('handleProjectStatus — live enrichment', () => {
631632
expect(mockGetEvaluator).not.toHaveBeenCalled();
632633
});
633634
});
635+
636+
describe('buildRuntimeInvocationUrl', () => {
637+
it('constructs the correct invocation URL with encoded ARN', () => {
638+
const url = buildRuntimeInvocationUrl(
639+
'us-east-1',
640+
'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/travelplanner_FlightsMcp-abcdefgh'
641+
);
642+
expect(url).toBe(
643+
'https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aus-east-1%3A123456789012%3Aruntime%2Ftravelplanner_FlightsMcp-abcdefgh/invocations'
644+
);
645+
});
646+
647+
it('handles different regions', () => {
648+
const url = buildRuntimeInvocationUrl(
649+
'eu-west-1',
650+
'arn:aws:bedrock-agentcore:eu-west-1:111111111111:runtime/my-agent-xyz'
651+
);
652+
expect(url).toBe(
653+
'https://bedrock-agentcore.eu-west-1.amazonaws.com/runtimes/arn%3Aaws%3Abedrock-agentcore%3Aeu-west-1%3A111111111111%3Aruntime%2Fmy-agent-xyz/invocations'
654+
);
655+
});
656+
});
657+
658+
describe('handleProjectStatus — invocation URL enrichment', () => {
659+
beforeEach(() => {
660+
mockGetAgentRuntimeStatus.mockReset();
661+
mockGetEvaluator.mockReset();
662+
mockGetOnlineEvaluationConfig.mockReset();
663+
});
664+
665+
afterEach(() => vi.clearAllMocks());
666+
667+
it('sets invocationUrl on deployed agents after runtime status enrichment', async () => {
668+
const runtimeArn = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/proj_MyAgent-abc123';
669+
670+
mockGetAgentRuntimeStatus.mockResolvedValue({
671+
runtimeId: 'proj_MyAgent-abc123',
672+
status: 'READY',
673+
});
674+
675+
const ctx: StatusContext = {
676+
project: {
677+
...baseProject,
678+
runtimes: [{ name: 'MyAgent' }],
679+
} as unknown as AgentCoreProjectSpec,
680+
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
681+
deployedState: {
682+
targets: {
683+
dev: {
684+
resources: {
685+
runtimes: {
686+
MyAgent: {
687+
runtimeId: 'proj_MyAgent-abc123',
688+
runtimeArn,
689+
roleArn: 'arn:aws:iam::123456789012:role/test',
690+
},
691+
},
692+
},
693+
},
694+
},
695+
},
696+
} as unknown as StatusContext;
697+
698+
const result = await handleProjectStatus(ctx);
699+
700+
expect(result.success).toBe(true);
701+
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'MyAgent');
702+
expect(agentEntry).toBeDefined();
703+
expect(agentEntry!.invocationUrl).toBe(
704+
`https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/${encodeURIComponent(runtimeArn)}/invocations`
705+
);
706+
});
707+
708+
it('does not set invocationUrl on local-only agents', async () => {
709+
const ctx: StatusContext = {
710+
project: {
711+
...baseProject,
712+
runtimes: [{ name: 'LocalAgent' }],
713+
} as unknown as AgentCoreProjectSpec,
714+
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
715+
deployedState: {
716+
targets: {
717+
dev: {
718+
resources: {},
719+
},
720+
},
721+
},
722+
} as unknown as StatusContext;
723+
724+
const result = await handleProjectStatus(ctx);
725+
726+
expect(result.success).toBe(true);
727+
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'LocalAgent');
728+
expect(agentEntry).toBeDefined();
729+
expect(agentEntry!.invocationUrl).toBeUndefined();
730+
});
731+
732+
it('still sets invocationUrl when runtime status fetch fails', async () => {
733+
const runtimeArn = 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/proj_FailAgent-xyz';
734+
mockGetAgentRuntimeStatus.mockRejectedValue(new Error('Timeout'));
735+
736+
const ctx: StatusContext = {
737+
project: {
738+
...baseProject,
739+
runtimes: [{ name: 'FailAgent' }],
740+
} as unknown as AgentCoreProjectSpec,
741+
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
742+
deployedState: {
743+
targets: {
744+
dev: {
745+
resources: {
746+
runtimes: {
747+
FailAgent: {
748+
runtimeId: 'proj_FailAgent-xyz',
749+
runtimeArn,
750+
roleArn: 'arn:aws:iam::123456789012:role/test',
751+
},
752+
},
753+
},
754+
},
755+
},
756+
},
757+
} as unknown as StatusContext;
758+
759+
const result = await handleProjectStatus(ctx);
760+
761+
expect(result.success).toBe(true);
762+
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'FailAgent');
763+
expect(agentEntry).toBeDefined();
764+
expect(agentEntry!.error).toBe('Timeout');
765+
expect(agentEntry!.invocationUrl).toBe(
766+
`https://bedrock-agentcore.us-east-1.amazonaws.com/runtimes/${encodeURIComponent(runtimeArn)}/invocations`
767+
);
768+
});
769+
770+
it('does not set invocationUrl on pending-removal agents', async () => {
771+
mockGetAgentRuntimeStatus.mockResolvedValue({
772+
runtimeId: 'proj_OldAgent-abc',
773+
status: 'READY',
774+
});
775+
776+
const ctx: StatusContext = {
777+
project: {
778+
...baseProject,
779+
runtimes: [],
780+
} as unknown as AgentCoreProjectSpec,
781+
awsTargets: [{ name: 'dev', region: 'us-east-1', account: '123456789012' }],
782+
deployedState: {
783+
targets: {
784+
dev: {
785+
resources: {
786+
runtimes: {
787+
OldAgent: {
788+
runtimeId: 'proj_OldAgent-abc',
789+
runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/proj_OldAgent-abc',
790+
roleArn: 'arn:aws:iam::123456789012:role/test',
791+
},
792+
},
793+
},
794+
},
795+
},
796+
},
797+
} as unknown as StatusContext;
798+
799+
const result = await handleProjectStatus(ctx);
800+
801+
expect(result.success).toBe(true);
802+
const agentEntry = result.resources.find(r => r.resourceType === 'agent' && r.name === 'OldAgent');
803+
expect(agentEntry).toBeDefined();
804+
expect(agentEntry!.deploymentState).toBe('pending-removal');
805+
expect(agentEntry!.invocationUrl).toBeUndefined();
806+
});
807+
});

src/cli/commands/status/action.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getEvaluator, getOnlineEvaluationConfig } from '../../aws/agentcore-con
55
import { getErrorMessage } from '../../errors';
66
import { ExecLogger } from '../../logging';
77
import type { ResourceDeploymentState } from './constants';
8+
import { buildRuntimeInvocationUrl } from './constants';
89

910
export type { ResourceDeploymentState };
1011

@@ -23,6 +24,7 @@ export interface ResourceStatusEntry {
2324
identifier?: string;
2425
detail?: string;
2526
error?: string;
27+
invocationUrl?: string;
2628
}
2729

2830
export interface ProjectStatusResult {
@@ -284,16 +286,20 @@ export async function handleProjectStatus(
284286
const agentState = agentStates[entry.name];
285287
if (!agentState) return;
286288

289+
const invocationUrl = entry.identifier
290+
? buildRuntimeInvocationUrl(targetConfig.region, entry.identifier)
291+
: undefined;
292+
287293
try {
288294
const runtimeStatus = await getAgentRuntimeStatus({
289295
region: targetConfig.region,
290296
runtimeId: agentState.runtimeId,
291297
});
292-
resources[i] = { ...entry, detail: runtimeStatus.status };
298+
resources[i] = { ...entry, detail: runtimeStatus.status, invocationUrl };
293299
logger.log(` ${entry.name}: ${runtimeStatus.status} (${agentState.runtimeId})`);
294300
} catch (error) {
295301
const errorMsg = getErrorMessage(error);
296-
resources[i] = { ...entry, error: errorMsg };
302+
resources[i] = { ...entry, error: errorMsg, invocationUrl };
297303
logger.log(` ${entry.name}: ERROR - ${errorMsg}`, 'error');
298304
}
299305
})

src/cli/commands/status/command.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,14 @@ export const registerStatus = (program: Command) => {
154154
<Box flexDirection="column" marginTop={1}>
155155
<Text bold>Agents</Text>
156156
{agents.map(entry => (
157-
<ResourceEntry key={`${entry.resourceType}-${entry.name}`} entry={entry} showRuntime />
157+
<Box key={`${entry.resourceType}-${entry.name}`} flexDirection="column">
158+
<ResourceEntry entry={entry} showRuntime />
159+
{entry.invocationUrl && (
160+
<Text dimColor>
161+
{' '}URL: {entry.invocationUrl}
162+
</Text>
163+
)}
164+
</Box>
158165
))}
159166
</Box>
160167
)}

src/cli/commands/status/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ export const DEPLOYMENT_STATE_LABELS: Record<ResourceDeploymentState, string> =
1313
'local-only': 'Local only',
1414
'pending-removal': 'Removed locally',
1515
};
16+
17+
export function buildRuntimeInvocationUrl(region: string, runtimeArn: string): string {
18+
const encodedArn = encodeURIComponent(runtimeArn);
19+
return `https://bedrock-agentcore.${region}.amazonaws.com/runtimes/${encodedArn}/invocations`;
20+
}

src/cli/tui/components/ResourceGraph.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function ResourceRow({
7272
statusColor,
7373
deploymentState,
7474
identifier,
75+
invocationUrl,
7576
}: {
7677
icon: string;
7778
color: string;
@@ -81,6 +82,7 @@ function ResourceRow({
8182
statusColor?: string;
8283
deploymentState?: ResourceStatusEntry['deploymentState'];
8384
identifier?: string;
85+
invocationUrl?: string;
8486
}) {
8587
const badge = deploymentState ? getDeploymentBadge(deploymentState) : undefined;
8688

@@ -98,6 +100,11 @@ function ResourceRow({
98100
{' '}ID: {identifier}
99101
</Text>
100102
)}
103+
{invocationUrl && (
104+
<Text dimColor>
105+
{' '}URL: {invocationUrl}
106+
</Text>
107+
)}
101108
</Box>
102109
);
103110
}
@@ -182,6 +189,7 @@ export function ResourceGraph({ project, mcp, agentName, resourceStatuses }: Res
182189
statusColor={runtimeStatusColor}
183190
deploymentState={rsEntry?.deploymentState}
184191
identifier={rsEntry?.identifier}
192+
invocationUrl={rsEntry?.invocationUrl}
185193
/>
186194
);
187195
})}

0 commit comments

Comments
 (0)