Skip to content

Commit 6cc575c

Browse files
authored
feat: add agent inspector web UI for agentcore dev (#871)
1 parent abfd33b commit 6cc575c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+5887
-2903
lines changed

package-lock.json

Lines changed: 2101 additions & 2832 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
"@aws-sdk/client-xray": "^3.1003.0",
8888
"@aws-sdk/credential-providers": "^3.893.0",
8989
"@commander-js/extra-typings": "^14.0.0",
90+
"@opentelemetry/api": "^1.9.0",
91+
"@opentelemetry/otlp-transformer": "^0.213.0",
9092
"@smithy/shared-ini-file-loader": "^4.4.2",
9193
"commander": "^14.0.2",
9294
"dotenv": "^17.2.3",
@@ -98,6 +100,7 @@
98100
"js-yaml": "^4.1.1",
99101
"react": "^19.2.3",
100102
"yaml": "^2.8.3",
103+
"@aws/agent-inspector": "0.1.0",
101104
"zod": "^4.3.5"
102105
},
103106
"peerDependencies": {

scripts/copy-assets.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const __dirname = path.dirname(__filename);
77

88
const srcDir = path.join(__dirname, '..', 'src', 'assets');
99
const destDir = path.join(__dirname, '..', 'dist', 'assets');
10+
const inspectorSrcDir = path.join(__dirname, '..', 'node_modules', '@aws', 'agent-inspector', 'dist-assets');
11+
const inspectorDestDir = path.join(__dirname, '..', 'dist', 'agent-inspector');
1012

1113
/**
1214
* Recursively copy directory contents, excluding specified files at root level only
@@ -44,6 +46,17 @@ try {
4446
console.log('Copying assets...');
4547
copyDir(srcDir, destDir, ['AGENTS.md']);
4648
console.log('Assets copied successfully!');
49+
50+
// Copy @aws/agent-inspector built assets into dist/agent-inspector/ for bundled CLI
51+
if (fs.existsSync(inspectorSrcDir)) {
52+
console.log('Copying @aws/agent-inspector assets...');
53+
copyDir(inspectorSrcDir, inspectorDestDir);
54+
console.log('@aws/agent-inspector assets copied successfully!');
55+
} else {
56+
console.warn(
57+
'Warning: @aws/agent-inspector dist-assets/ not found — skipping. Run "npm run build" in the agent-inspector package.'
58+
);
59+
}
4760
} catch (error) {
4861
console.error('Error copying assets:', error);
4962
process.exit(1);

src/cli/cli.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { ALL_PRIMITIVES } from './primitives';
2424
import { App } from './tui/App';
2525
import { LayoutProvider } from './tui/context';
2626
import { COMMAND_DESCRIPTIONS } from './tui/copy';
27+
import { clearExitAction, getExitAction } from './tui/exit-action';
2728
import { clearExitMessage, getExitMessage } from './tui/exit-message';
2829
import { CommandListScreen } from './tui/screens/home';
2930
import { getCommandsForUI } from './tui/utils';
@@ -104,6 +105,16 @@ function renderTUI(updateCheck: Promise<UpdateCheckResult | null>, isFirstRun: b
104105
process.stdout.write(EXIT_ALT_SCREEN);
105106
process.stdout.write(SHOW_CURSOR);
106107

108+
// Check if the TUI requested a post-exit action (e.g., launch browser dev mode)
109+
const action = getExitAction();
110+
clearExitAction();
111+
112+
if (action?.type === 'dev') {
113+
const { launchBrowserDev } = await import('./commands/dev/browser-mode');
114+
await launchBrowserDev();
115+
return;
116+
}
117+
107118
// Print any exit message set by screens (e.g., after successful project creation)
108119
const exitMessage = getExitMessage();
109120
if (exitMessage) {
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { ConfigIO, findConfigRoot, getWorkingDirectory } from '../../../lib';
2+
import type { AgentCoreProjectSpec } from '../../../schema';
3+
import { getDevConfig, getDevSupportedAgents, loadDevEnv, loadProjectConfig } from '../../operations/dev';
4+
import { type OtelCollector, startOtelCollector } from '../../operations/dev/otel';
5+
import {
6+
type AgentInfo,
7+
type ListMemoryRecordsHandler,
8+
type RetrieveMemoryRecordsHandler,
9+
runWebUI,
10+
} from '../../operations/dev/web-ui';
11+
import { listMemoryRecords, retrieveMemoryRecords } from '../../operations/memory';
12+
import path from 'node:path';
13+
14+
interface DeployedHandlers {
15+
onListMemoryRecords?: ListMemoryRecordsHandler;
16+
onRetrieveMemoryRecords?: RetrieveMemoryRecordsHandler;
17+
}
18+
19+
/**
20+
* Resolve deployed resources (memories, agents) from config and return handlers
21+
* that query them via the AWS SDK. Only resources with "deployed" status are available.
22+
*/
23+
async function resolveDeployedHandlers(
24+
baseDir: string,
25+
onLog: (level: 'info' | 'warn' | 'error', msg: string) => void
26+
): Promise<DeployedHandlers> {
27+
const configIO = new ConfigIO({ baseDir });
28+
29+
if (!configIO.configExists('state') || !configIO.configExists('awsTargets')) {
30+
return {};
31+
}
32+
33+
try {
34+
const deployedState = await configIO.readDeployedState();
35+
const awsTargets = await configIO.readAWSDeploymentTargets();
36+
37+
const targetName = Object.keys(deployedState.targets)[0];
38+
if (!targetName) return {};
39+
40+
const targetState = deployedState.targets[targetName];
41+
const targetConfig = awsTargets.find(t => t.name === targetName);
42+
if (!targetConfig) return {};
43+
44+
const region = targetConfig.region;
45+
const result: DeployedHandlers = {};
46+
47+
// Memory handlers
48+
const memoryEntries = targetState?.resources?.memories ?? {};
49+
const memories = Object.entries(memoryEntries).map(([name, state]) => ({
50+
name,
51+
memoryId: state.memoryId,
52+
region,
53+
}));
54+
55+
if (memories.length > 0) {
56+
onLog(
57+
'info',
58+
`Memory browsing enabled for ${memories.length} deployed memory(ies): ${memories.map(m => m.name).join(', ')}`
59+
);
60+
61+
result.onListMemoryRecords = async (memoryName, namespace, strategyId) => {
62+
const memory = memories.find(m => m.name === memoryName);
63+
if (!memory) return { success: false, error: `Memory "${memoryName}" not found in deployed state` };
64+
return listMemoryRecords({
65+
region: memory.region,
66+
memoryId: memory.memoryId,
67+
namespace,
68+
memoryStrategyId: strategyId,
69+
});
70+
};
71+
72+
result.onRetrieveMemoryRecords = async (memoryName, namespace, searchQuery, strategyId) => {
73+
const memory = memories.find(m => m.name === memoryName);
74+
if (!memory) return { success: false, error: `Memory "${memoryName}" not found in deployed state` };
75+
return retrieveMemoryRecords({
76+
region: memory.region,
77+
memoryId: memory.memoryId,
78+
namespace,
79+
searchQuery,
80+
memoryStrategyId: strategyId,
81+
});
82+
};
83+
}
84+
85+
return result;
86+
} catch (err) {
87+
onLog('warn', `Could not resolve deployed resources: ${err instanceof Error ? err.message : String(err)}`);
88+
return {};
89+
}
90+
}
91+
92+
export interface BrowserModeOptions {
93+
workingDir: string;
94+
project: AgentCoreProjectSpec;
95+
port: number;
96+
agentName?: string;
97+
/** OTEL env vars to pass to dev servers (set by the dev command when collector is active) */
98+
otelEnvVars?: Record<string, string>;
99+
/** OTEL collector instance for local trace collection */
100+
collector?: OtelCollector;
101+
}
102+
103+
/**
104+
* Standalone entry point for launching browser dev mode from the TUI.
105+
* Handles all setup (project loading, OTEL collector, etc.) internally.
106+
*/
107+
export async function launchBrowserDev(): Promise<void> {
108+
const workingDir = getWorkingDirectory();
109+
const project = await loadProjectConfig(workingDir);
110+
111+
if (!project?.runtimes || project.runtimes.length === 0) {
112+
console.error('Error: No agents defined in project.');
113+
process.exit(1);
114+
}
115+
116+
const configRoot = findConfigRoot(workingDir);
117+
const persistTracesDir = path.join(configRoot ?? workingDir, '.cli', 'traces');
118+
const { collector, otelEnvVars } = await startOtelCollector(persistTracesDir);
119+
120+
await runBrowserMode({
121+
workingDir,
122+
project,
123+
port: 8080,
124+
otelEnvVars,
125+
collector,
126+
});
127+
}
128+
129+
export async function runBrowserMode(opts: BrowserModeOptions): Promise<void> {
130+
const { workingDir, project, agentName, otelEnvVars = {}, collector } = opts;
131+
132+
const configRoot = findConfigRoot(workingDir);
133+
const { envVars } = await loadDevEnv(workingDir);
134+
135+
const supportedAgents = getDevSupportedAgents(project);
136+
137+
if (supportedAgents.length === 0) {
138+
console.error('Error: No dev-supported agents found.');
139+
process.exit(1);
140+
}
141+
142+
if (agentName && !supportedAgents.some(a => a.name === agentName)) {
143+
console.error(`Error: Agent "${agentName}" not found or does not support dev mode.`);
144+
process.exit(1);
145+
}
146+
147+
const onLog = (level: 'info' | 'warn' | 'error', msg: string) => {
148+
if (level === 'error') console.error(`Web UI: ${msg}`);
149+
};
150+
151+
const mergedEnvVars = { ...envVars, ...otelEnvVars };
152+
153+
const agentInfoList: AgentInfo[] = supportedAgents.map(a => ({
154+
name: a.name,
155+
buildType: a.build,
156+
protocol: a.protocol ?? 'HTTP',
157+
}));
158+
159+
// Resolve deployed resources (memories, agents) so memory browsing and
160+
// CloudWatch traces work in dev mode alongside local traces.
161+
// Handlers re-resolve on each call so newly deployed memories are picked up.
162+
const baseDir = configRoot ?? workingDir;
163+
164+
await runWebUI({
165+
logLabel: 'dev',
166+
onLog,
167+
serverOptions: {
168+
mode: 'dev',
169+
agents: agentInfoList,
170+
selectedAgent: agentName,
171+
envVars: mergedEnvVars,
172+
getEnvVars: async () => {
173+
const { envVars: freshEnvVars } = await loadDevEnv(workingDir);
174+
return { ...freshEnvVars, ...otelEnvVars };
175+
},
176+
configRoot: configRoot ?? undefined,
177+
getDevConfig: async name => {
178+
const freshProject = await loadProjectConfig(workingDir);
179+
return getDevConfig(workingDir, freshProject, configRoot ?? undefined, name);
180+
},
181+
reloadAgents: configRoot
182+
? async () => {
183+
const freshProject = await loadProjectConfig(workingDir);
184+
return getDevSupportedAgents(freshProject).map(a => ({
185+
name: a.name,
186+
buildType: a.build,
187+
protocol: a.protocol ?? 'HTTP',
188+
}));
189+
}
190+
: undefined,
191+
onListTraces: collector
192+
? (agentNameParam, startTime, endTime) => collector.listTraces(agentNameParam, startTime, endTime)
193+
: undefined,
194+
onGetTrace: collector ? (agentNameParam, traceId) => collector.getTraceSpans(agentNameParam, traceId) : undefined,
195+
onListMemoryRecords: async (memoryName, namespace, strategyId) => {
196+
const deployed = await resolveDeployedHandlers(baseDir, onLog);
197+
if (!deployed.onListMemoryRecords) return { success: false, error: 'No deployed AgentCore Memory found' };
198+
return deployed.onListMemoryRecords(memoryName, namespace, strategyId);
199+
},
200+
onRetrieveMemoryRecords: async (memoryName, namespace, searchQuery, strategyId) => {
201+
const deployed = await resolveDeployedHandlers(baseDir, onLog);
202+
if (!deployed.onRetrieveMemoryRecords) return { success: false, error: 'No deployed AgentCore Memory found' };
203+
return deployed.onRetrieveMemoryRecords(memoryName, namespace, searchQuery, strategyId);
204+
},
205+
},
206+
});
207+
}

0 commit comments

Comments
 (0)