diff --git a/e2e-tests/copilotkit-frontend.test.ts b/e2e-tests/copilotkit-frontend.test.ts new file mode 100644 index 000000000..7bdc5c176 --- /dev/null +++ b/e2e-tests/copilotkit-frontend.test.ts @@ -0,0 +1,172 @@ +import { parseJsonOutput, prereqs, spawnAndCollect } from '../src/test-utils/index.js'; +import { runAgentCoreCLI } from './e2e-helper.js'; +import { randomUUID } from 'node:crypto'; +import { existsSync } from 'node:fs'; +import { mkdir, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const canRun = prereqs.npm && prereqs.git && prereqs.uv; + +describe.sequential('e2e: CopilotKit frontend scaffolding', () => { + let testDir: string; + + beforeAll(async () => { + if (!canRun) return; + testDir = join(tmpdir(), `agentcore-e2e-cpk-${randomUUID()}`); + await mkdir(testDir, { recursive: true }); + }, 30000); + + afterAll(async () => { + if (testDir) { + await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 }); + } + }, 30000); + + it.skipIf(!canRun)( + 'create with --frontend copilotkit scaffolds frontend directory', + async () => { + const name = 'CpkYes'; + const result = await runAgentCoreCLI( + [ + 'create', + '--name', + name, + '--language', + 'Python', + '--protocol', + 'AGUI', + '--framework', + 'Strands', + '--model-provider', + 'Bedrock', + '--memory', + 'none', + '--frontend', + 'copilotkit', + '--skip-git', + '--skip-install', + '--skip-python-setup', + '--json', + ], + testDir + ); + + expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0); + const json = parseJsonOutput(result.stdout) as { projectPath: string }; + const frontendDir = join(json.projectPath, 'app', name, 'frontend'); + + expect(existsSync(frontendDir), 'frontend/ directory should exist').toBe(true); + expect(existsSync(join(frontendDir, 'package.json'))).toBe(true); + expect(existsSync(join(frontendDir, 'index.html'))).toBe(true); + expect(existsSync(join(frontendDir, 'tsconfig.json'))).toBe(true); + expect(existsSync(join(frontendDir, 'vite.config.ts'))).toBe(true); + expect(existsSync(join(frontendDir, 'src', 'App.tsx'))).toBe(true); + expect(existsSync(join(frontendDir, 'src', 'bridge.ts'))).toBe(true); + expect(existsSync(join(frontendDir, 'src', 'main.tsx'))).toBe(true); + }, + 60000 + ); + + it.skipIf(!canRun)( + 'create without --frontend does not scaffold frontend directory', + async () => { + const name = 'CpkNo'; + const result = await runAgentCoreCLI( + [ + 'create', + '--name', + name, + '--language', + 'Python', + '--protocol', + 'AGUI', + '--framework', + 'Strands', + '--model-provider', + 'Bedrock', + '--memory', + 'none', + '--skip-git', + '--skip-install', + '--skip-python-setup', + '--json', + ], + testDir + ); + + expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0); + const json = parseJsonOutput(result.stdout) as { projectPath: string }; + const frontendDir = join(json.projectPath, 'app', name, 'frontend'); + + expect(existsSync(frontendDir), 'frontend/ directory should NOT exist').toBe(false); + }, + 60000 + ); + + it.skipIf(!canRun)( + 'frontend/package.json has correct name substitution', + async () => { + const name = 'CpkName'; + const result = await runAgentCoreCLI( + [ + 'create', + '--name', + name, + '--language', + 'Python', + '--protocol', + 'AGUI', + '--framework', + 'Strands', + '--model-provider', + 'Bedrock', + '--memory', + 'none', + '--frontend', + 'copilotkit', + '--skip-git', + '--skip-install', + '--skip-python-setup', + '--json', + ], + testDir + ); + + expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0); + const json = parseJsonOutput(result.stdout) as { projectPath: string }; + const pkgPath = join(json.projectPath, 'app', name, 'frontend', 'package.json'); + const pkg = JSON.parse(await readFile(pkgPath, 'utf-8')); + + expect(pkg.name).toBe(`${name}-frontend`); + }, + 60000 + ); + + it.skipIf(!canRun)( + 'frontend/index.html has correct title substitution', + async () => { + const name = 'CpkName'; + const projectPath = join(testDir, name); + const indexPath = join(projectPath, 'app', name, 'frontend', 'index.html'); + const html = await readFile(indexPath, 'utf-8'); + + expect(html).toContain(`${name}`); + }, + 10000 + ); + + it.skipIf(!canRun)( + 'npm install succeeds in scaffolded frontend', + async () => { + const name = 'CpkName'; + const projectPath = join(testDir, name); + const frontendDir = join(projectPath, 'app', name, 'frontend'); + + const result = await spawnAndCollect('npm', ['install'], frontendDir); + expect(result.exitCode, `npm install failed: ${result.stderr}`).toBe(0); + }, + 120000 + ); +}); diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index a97bbeb1d..e950038aa 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -450,6 +450,13 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "evaluators/python-lambda/execution-role-policy.json", "evaluators/python-lambda/lambda_function.py", "evaluators/python-lambda/pyproject.toml", + "frontend/copilotkit/index.html", + "frontend/copilotkit/package.json", + "frontend/copilotkit/src/App.tsx", + "frontend/copilotkit/src/bridge.ts", + "frontend/copilotkit/src/main.tsx", + "frontend/copilotkit/tsconfig.json", + "frontend/copilotkit/vite.config.ts", "mcp/python-lambda/README.md", "mcp/python-lambda/handler.py", "mcp/python-lambda/pyproject.toml", @@ -546,6 +553,138 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f ] `; +exports[`Assets Directory Snapshots > Frontend assets > frontend/frontend/copilotkit/index.html should match snapshot 1`] = ` +" + + + + + {{ name }} + + +
+ + + +" +`; + +exports[`Assets Directory Snapshots > Frontend assets > frontend/frontend/copilotkit/package.json should match snapshot 1`] = ` +"{ + "name": "{{ name }}-frontend", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently -n bridge,ui -c blue,green \\"tsx watch src/bridge.ts\\" \\"vite\\"", + "build": "tsc && vite build" + }, + "dependencies": { + "@copilotkit/react-core": "^1.54.0", + "@ag-ui/client": "^0.0.47", + "@copilotkit/runtime": "^1.54.0", + "@hono/node-server": "^1.13.7", + "hono": "^4.11.4", + "react": "^19.2.1", + "react-dom": "^19.2.1" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^9.0.0", + "tsx": "^4.19.0", + "typescript": "^5.0.0", + "vite": "^6.0.0" + } +} +" +`; + +exports[`Assets Directory Snapshots > Frontend assets > frontend/frontend/copilotkit/src/App.tsx should match snapshot 1`] = ` +"import { CopilotChat, CopilotKitProvider } from '@copilotkit/react-core/v2'; +import '@copilotkit/react-core/v2/styles.css'; + +export default function App() { + return ( + + + + ); +} +" +`; + +exports[`Assets Directory Snapshots > Frontend assets > frontend/frontend/copilotkit/src/bridge.ts should match snapshot 1`] = ` +"import { HttpAgent } from '@ag-ui/client'; +import { CopilotRuntime, createCopilotEndpoint } from '@copilotkit/runtime/v2'; +import { serve } from '@hono/node-server'; + +const AGENT_PORT = process.env.AGENT_PORT ?? '8080'; +const AGENT_URL = process.env.AGENT_URL ?? \`http://localhost:\${AGENT_PORT}/invocations\`; + +const agent = new HttpAgent({ url: AGENT_URL, headers: {} }); +const runtime = new CopilotRuntime({ agents: { default: agent } }); +const app = createCopilotEndpoint({ runtime, basePath: '/copilotkit' }); + +serve({ fetch: app.fetch, port: 3001 }, () => { + console.log(\`CopilotKit bridge → \${AGENT_URL}\`); + console.log('Bridge running on http://localhost:3001/copilotkit'); +}); +" +`; + +exports[`Assets Directory Snapshots > Frontend assets > frontend/frontend/copilotkit/src/main.tsx should match snapshot 1`] = ` +"import App from './App'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +createRoot(document.getElementById('root')!).render( + + + +); +" +`; + +exports[`Assets Directory Snapshots > Frontend assets > frontend/frontend/copilotkit/tsconfig.json should match snapshot 1`] = ` +"{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src"] +} +" +`; + +exports[`Assets Directory Snapshots > Frontend assets > frontend/frontend/copilotkit/vite.config.ts should match snapshot 1`] = ` +"import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + open: true, + }, +}); +" +`; + exports[`Assets Directory Snapshots > MCP assets > mcp/mcp/python/README.md should match snapshot 1`] = ` "# {{ name }} @@ -1961,6 +2100,34 @@ GET /ping Returns \`{"status": "healthy"}\`. +{{#if hasFrontend}} +## Frontend (CopilotKit) + +A React chat interface that connects to your agent via the AG-UI protocol. + +Start your agent in one terminal: + +\`\`\`bash +agentcore dev --logs +\`\`\` + +Then in another terminal: + +\`\`\`bash +cd frontend +npm install +npm run dev +\`\`\` + +Open http://localhost:3000. + +The frontend connects to \`http://localhost:8080/invocations\` by default. If your agent runs on a different port (check the \`agentcore dev --logs\` output), set \`AGENT_PORT\`: + +\`\`\`bash +AGENT_PORT=8081 npm run dev +\`\`\` + +{{/if}} ## Deploy \`\`\`bash @@ -2008,6 +2175,9 @@ env/ *.swo *~ +# Frontend +node_modules/ + # OS .DS_Store Thumbs.db @@ -2145,6 +2315,34 @@ uv run python main.py The agent starts on port 8080. +{{#if hasFrontend}} +## Frontend (CopilotKit) + +A React chat interface that connects to your agent via the AG-UI protocol. + +Start your agent in one terminal: + +\`\`\`bash +agentcore dev --logs +\`\`\` + +Then in another terminal: + +\`\`\`bash +cd frontend +npm install +npm run dev +\`\`\` + +Open http://localhost:3000. + +The frontend connects to \`http://localhost:8080/invocations\` by default. If your agent runs on a different port (check the \`agentcore dev --logs\` output), set \`AGENT_PORT\`: + +\`\`\`bash +AGENT_PORT=8081 npm run dev +\`\`\` + +{{/if}} ## Deploy \`\`\`bash @@ -2192,6 +2390,9 @@ env/ *.swo *~ +# Frontend +node_modules/ + # OS .DS_Store Thumbs.db @@ -2460,6 +2661,34 @@ uv run python main.py The agent starts on port 8080. +{{#if hasFrontend}} +## Frontend (CopilotKit) + +A React chat interface that connects to your agent via the AG-UI protocol. + +Start your agent in one terminal: + +\`\`\`bash +agentcore dev --logs +\`\`\` + +Then in another terminal: + +\`\`\`bash +cd frontend +npm install +npm run dev +\`\`\` + +Open http://localhost:3000. + +The frontend connects to \`http://localhost:8080/invocations\` by default. If your agent runs on a different port (check the \`agentcore dev --logs\` output), set \`AGENT_PORT\`: + +\`\`\`bash +AGENT_PORT=8081 npm run dev +\`\`\` + +{{/if}} ## Deploy \`\`\`bash @@ -2507,6 +2736,9 @@ env/ *.swo *~ +# Frontend +node_modules/ + # OS .DS_Store Thumbs.db diff --git a/src/assets/__tests__/assets.snapshot.test.ts b/src/assets/__tests__/assets.snapshot.test.ts index e0d3735cf..a418a0d49 100644 --- a/src/assets/__tests__/assets.snapshot.test.ts +++ b/src/assets/__tests__/assets.snapshot.test.ts @@ -87,6 +87,15 @@ describe('Assets Directory Snapshots', () => { }); }); + describe('Frontend assets', () => { + const frontendFiles = assetFiles.filter(f => f.startsWith('frontend/')); + + it.each(frontendFiles)('frontend/%s should match snapshot', file => { + const content = readFileContent(path.join(ASSETS_DIR, file)); + expect(content).toMatchSnapshot(); + }); + }); + describe.skipIf(assetFiles.filter(f => f.startsWith('static/')).length === 0)('Static assets', () => { const staticFiles = assetFiles.filter(f => f.startsWith('static/')); diff --git a/src/assets/frontend/copilotkit/index.html b/src/assets/frontend/copilotkit/index.html new file mode 100644 index 000000000..ebdb1a0f2 --- /dev/null +++ b/src/assets/frontend/copilotkit/index.html @@ -0,0 +1,12 @@ + + + + + + {{ name }} + + +
+ + + diff --git a/src/assets/frontend/copilotkit/package.json b/src/assets/frontend/copilotkit/package.json new file mode 100644 index 000000000..a63b5b699 --- /dev/null +++ b/src/assets/frontend/copilotkit/package.json @@ -0,0 +1,27 @@ +{ + "name": "{{ name }}-frontend", + "private": true, + "type": "module", + "scripts": { + "dev": "concurrently -n bridge,ui -c blue,green \"tsx watch src/bridge.ts\" \"vite\"", + "build": "tsc && vite build" + }, + "dependencies": { + "@copilotkit/react-core": "^1.54.0", + "@ag-ui/client": "^0.0.47", + "@copilotkit/runtime": "^1.54.0", + "@hono/node-server": "^1.13.7", + "hono": "^4.11.4", + "react": "^19.2.1", + "react-dom": "^19.2.1" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.2.0", + "concurrently": "^9.0.0", + "tsx": "^4.19.0", + "typescript": "^5.0.0", + "vite": "^6.0.0" + } +} diff --git a/src/assets/frontend/copilotkit/src/App.tsx b/src/assets/frontend/copilotkit/src/App.tsx new file mode 100644 index 000000000..c2844355a --- /dev/null +++ b/src/assets/frontend/copilotkit/src/App.tsx @@ -0,0 +1,10 @@ +import { CopilotChat, CopilotKitProvider } from '@copilotkit/react-core/v2'; +import '@copilotkit/react-core/v2/styles.css'; + +export default function App() { + return ( + + + + ); +} diff --git a/src/assets/frontend/copilotkit/src/bridge.ts b/src/assets/frontend/copilotkit/src/bridge.ts new file mode 100644 index 000000000..bcc3ce58d --- /dev/null +++ b/src/assets/frontend/copilotkit/src/bridge.ts @@ -0,0 +1,15 @@ +import { HttpAgent } from '@ag-ui/client'; +import { CopilotRuntime, createCopilotEndpoint } from '@copilotkit/runtime/v2'; +import { serve } from '@hono/node-server'; + +const AGENT_PORT = process.env.AGENT_PORT ?? '8080'; +const AGENT_URL = process.env.AGENT_URL ?? `http://localhost:${AGENT_PORT}/invocations`; + +const agent = new HttpAgent({ url: AGENT_URL, headers: {} }); +const runtime = new CopilotRuntime({ agents: { default: agent } }); +const app = createCopilotEndpoint({ runtime, basePath: '/copilotkit' }); + +serve({ fetch: app.fetch, port: 3001 }, () => { + console.log(`CopilotKit bridge → ${AGENT_URL}`); + console.log('Bridge running on http://localhost:3001/copilotkit'); +}); diff --git a/src/assets/frontend/copilotkit/src/main.tsx b/src/assets/frontend/copilotkit/src/main.tsx new file mode 100644 index 000000000..60feef333 --- /dev/null +++ b/src/assets/frontend/copilotkit/src/main.tsx @@ -0,0 +1,9 @@ +import App from './App'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/assets/frontend/copilotkit/tsconfig.json b/src/assets/frontend/copilotkit/tsconfig.json new file mode 100644 index 000000000..0dbb7cf5d --- /dev/null +++ b/src/assets/frontend/copilotkit/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src"] +} diff --git a/src/assets/frontend/copilotkit/vite.config.ts b/src/assets/frontend/copilotkit/vite.config.ts new file mode 100644 index 000000000..a2e3cf904 --- /dev/null +++ b/src/assets/frontend/copilotkit/vite.config.ts @@ -0,0 +1,10 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + open: true, + }, +}); diff --git a/src/assets/python/agui/googleadk/base/README.md b/src/assets/python/agui/googleadk/base/README.md index 5258d4b9a..5bfb05873 100644 --- a/src/assets/python/agui/googleadk/base/README.md +++ b/src/assets/python/agui/googleadk/base/README.md @@ -23,6 +23,34 @@ GET /ping Returns `{"status": "healthy"}`. +{{#if hasFrontend}} +## Frontend (CopilotKit) + +A React chat interface that connects to your agent via the AG-UI protocol. + +Start your agent in one terminal: + +```bash +agentcore dev --logs +``` + +Then in another terminal: + +```bash +cd frontend +npm install +npm run dev +``` + +Open http://localhost:3000. + +The frontend connects to `http://localhost:8080/invocations` by default. If your agent runs on a different port (check the `agentcore dev --logs` output), set `AGENT_PORT`: + +```bash +AGENT_PORT=8081 npm run dev +``` + +{{/if}} ## Deploy ```bash diff --git a/src/assets/python/agui/googleadk/base/gitignore.template b/src/assets/python/agui/googleadk/base/gitignore.template index fa1c60aea..90c9299cc 100644 --- a/src/assets/python/agui/googleadk/base/gitignore.template +++ b/src/assets/python/agui/googleadk/base/gitignore.template @@ -36,6 +36,9 @@ env/ *.swo *~ +# Frontend +node_modules/ + # OS .DS_Store Thumbs.db diff --git a/src/assets/python/agui/langchain_langgraph/base/README.md b/src/assets/python/agui/langchain_langgraph/base/README.md index d0b2793b3..8c21195b8 100644 --- a/src/assets/python/agui/langchain_langgraph/base/README.md +++ b/src/assets/python/agui/langchain_langgraph/base/README.md @@ -15,6 +15,34 @@ uv run python main.py The agent starts on port 8080. +{{#if hasFrontend}} +## Frontend (CopilotKit) + +A React chat interface that connects to your agent via the AG-UI protocol. + +Start your agent in one terminal: + +```bash +agentcore dev --logs +``` + +Then in another terminal: + +```bash +cd frontend +npm install +npm run dev +``` + +Open http://localhost:3000. + +The frontend connects to `http://localhost:8080/invocations` by default. If your agent runs on a different port (check the `agentcore dev --logs` output), set `AGENT_PORT`: + +```bash +AGENT_PORT=8081 npm run dev +``` + +{{/if}} ## Deploy ```bash diff --git a/src/assets/python/agui/langchain_langgraph/base/gitignore.template b/src/assets/python/agui/langchain_langgraph/base/gitignore.template index fa1c60aea..90c9299cc 100644 --- a/src/assets/python/agui/langchain_langgraph/base/gitignore.template +++ b/src/assets/python/agui/langchain_langgraph/base/gitignore.template @@ -36,6 +36,9 @@ env/ *.swo *~ +# Frontend +node_modules/ + # OS .DS_Store Thumbs.db diff --git a/src/assets/python/agui/strands/base/README.md b/src/assets/python/agui/strands/base/README.md index 57a8cda94..93592e24d 100644 --- a/src/assets/python/agui/strands/base/README.md +++ b/src/assets/python/agui/strands/base/README.md @@ -15,6 +15,34 @@ uv run python main.py The agent starts on port 8080. +{{#if hasFrontend}} +## Frontend (CopilotKit) + +A React chat interface that connects to your agent via the AG-UI protocol. + +Start your agent in one terminal: + +```bash +agentcore dev --logs +``` + +Then in another terminal: + +```bash +cd frontend +npm install +npm run dev +``` + +Open http://localhost:3000. + +The frontend connects to `http://localhost:8080/invocations` by default. If your agent runs on a different port (check the `agentcore dev --logs` output), set `AGENT_PORT`: + +```bash +AGENT_PORT=8081 npm run dev +``` + +{{/if}} ## Deploy ```bash diff --git a/src/assets/python/agui/strands/base/gitignore.template b/src/assets/python/agui/strands/base/gitignore.template index fa1c60aea..90c9299cc 100644 --- a/src/assets/python/agui/strands/base/gitignore.template +++ b/src/assets/python/agui/strands/base/gitignore.template @@ -36,6 +36,9 @@ env/ *.swo *~ +# Frontend +node_modules/ + # OS .DS_Store Thumbs.db diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index 0f97de294..e3fe141bb 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -149,6 +149,7 @@ export interface CreateWithAgentOptions { maxLifetime?: number; sessionStorageMountPath?: string; withConfigBundle?: boolean; + frontend?: 'none' | 'copilotkit'; skipGit?: boolean; skipInstall?: boolean; skipPythonSetup?: boolean; @@ -175,6 +176,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P maxLifetime: maxLifetimeOpt, sessionStorageMountPath, withConfigBundle, + frontend, skipGit, skipInstall, skipPythonSetup, @@ -251,14 +253,16 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P onProgress?.('Add agent to project', 'start'); const agentName = name; const isMcp = protocol === 'MCP'; - const resolvedFramework = isMcp ? ('Strands' as SDKFramework) : (framework ?? ('Strands' as SDKFramework)); - const resolvedModelProvider = isMcp - ? ('Bedrock' as ModelProvider) - : (modelProvider ?? ('Bedrock' as ModelProvider)); + const resolvedFramework = isMcp ? ('Strands' as SDKFramework) : (framework ?? 'Strands'); + const resolvedModelProvider = isMcp ? ('Bedrock' as ModelProvider) : (modelProvider ?? 'Bedrock'); + + if (frontend && frontend !== 'none' && protocol !== 'AGUI') { + throw new Error('--frontend is only supported with AGUI protocol agents'); + } const generateConfig = { projectName: agentName, - buildType: buildType ?? ('CodeZip' as BuildType), + buildType: buildType ?? 'CodeZip', sdk: resolvedFramework, modelProvider: resolvedModelProvider, apiKey, @@ -273,6 +277,7 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P ...(maxLifetimeOpt !== undefined && { maxLifetime: maxLifetimeOpt }), ...(sessionStorageMountPath && { sessionStorageMountPath }), ...(withConfigBundle && { withConfigBundle }), + ...(frontend && frontend !== 'none' && { frontend }), }; // Resolve credential strategy FIRST (new project has no existing credentials) diff --git a/src/cli/commands/create/command.tsx b/src/cli/commands/create/command.tsx index a21ece491..262545c5d 100644 --- a/src/cli/commands/create/command.tsx +++ b/src/cli/commands/create/command.tsx @@ -14,6 +14,7 @@ import { AgentType, Build, Framework, + Frontend, Language, Memory, ModelProvider as ModelProviderEnum, @@ -25,7 +26,13 @@ import { COMMAND_DESCRIPTIONS } from '../../tui/copy'; import { requireTTY } from '../../tui/guards'; import { CreateScreen } from '../../tui/screens/create'; import { parseCommaSeparatedList } from '../shared/vpc-utils'; -import { type ProgressCallback, createProject, createProjectWithAgent, getDryRunInfo } from './action'; +import { + type CreateWithAgentOptions, + type ProgressCallback, + createProject, + createProjectWithAgent, + getDryRunInfo, +} from './action'; import type { CreateOptions } from './types'; import { validateCreateOptions } from './validate'; import type { Command } from '@commander-js/extra-typings'; @@ -123,6 +130,7 @@ async function handleCreateCLI(options: CreateOptions): Promise { build: standardize(Build, options.build ?? 'codezip'), agent_type: standardize(AgentType, options.type ?? 'create'), network_mode: standardize(NetworkModeEnum, options.networkMode ?? 'public'), + frontend: standardize(Frontend, options.frontend ?? 'none'), has_agent: options.agent !== false, }; @@ -182,6 +190,7 @@ async function handleCreateCLI(options: CreateOptions): Promise { maxLifetime: options.maxLifetime ? Number(options.maxLifetime) : undefined, sessionStorageMountPath: options.sessionStorageMountPath, withConfigBundle: options.withConfigBundle, + frontend: options.frontend as CreateWithAgentOptions['frontend'], skipGit: options.skipGit, skipInstall: options.skipInstall, skipPythonSetup: options.skipPythonSetup, @@ -250,6 +259,7 @@ export const registerCreate = (program: Command) => { 'Absolute mount path for session filesystem storage under /mnt (e.g. /mnt/data) [non-interactive]' ) .option('--with-config-bundle', 'Create a config bundle wired into the agent template [preview] [non-interactive]') + .option('--frontend ', 'Frontend UI to scaffold: copilotkit or none (AGUI only) [non-interactive]') .option('--output-dir ', 'Output directory (default: current directory) [non-interactive]') .option('--skip-git', 'Skip git repository initialization [non-interactive]') .option('--skip-python-setup', 'Skip Python virtual environment setup [non-interactive]') @@ -290,7 +300,7 @@ export const registerCreate = (program: Command) => { if (hasAnyFlag) { // Default language to Python (only supported option) for CLI mode options.language = options.language ?? 'Python'; - await handleCreateCLI(options as CreateOptions); + await handleCreateCLI(options); } else { requireTTY(); handleCreateTUI(); diff --git a/src/cli/commands/create/types.ts b/src/cli/commands/create/types.ts index cd42c545b..9a5c23707 100644 --- a/src/cli/commands/create/types.ts +++ b/src/cli/commands/create/types.ts @@ -21,6 +21,7 @@ export interface CreateOptions extends VpcOptions { maxLifetime?: number | string; sessionStorageMountPath?: string; withConfigBundle?: boolean; + frontend?: string; outputDir?: string; skipGit?: boolean; skipPythonSetup?: boolean; diff --git a/src/cli/commands/create/validate.ts b/src/cli/commands/create/validate.ts index a59c7d752..e5ae7ccd5 100644 --- a/src/cli/commands/create/validate.ts +++ b/src/cli/commands/create/validate.ts @@ -24,6 +24,7 @@ export interface ValidationResult { } const MEMORY_OPTIONS = ['none', 'shortTerm', 'longAndShortTerm'] as const; +const FRONTEND_OPTIONS = ['none', 'copilotkit'] as const; /** Check if a folder with the given name already exists in the directory */ export function validateFolderNotExists(name: string, cwd: string): true | string { @@ -202,6 +203,22 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val error: `Invalid memory option: ${options.memory}. Use none, shortTerm, or longAndShortTerm`, }; } + + // Validate frontend option + if (options.frontend) { + if (!FRONTEND_OPTIONS.includes(options.frontend as (typeof FRONTEND_OPTIONS)[number])) { + return { + valid: false, + error: `Invalid frontend option: ${options.frontend}. Use none or copilotkit`, + }; + } + if (options.frontend !== 'none' && protocol !== 'AGUI') { + return { + valid: false, + error: '--frontend is only supported with AGUI protocol', + }; + } + } } // Validate VPC options diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index 3ed449236..0c69cade3 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -285,5 +285,6 @@ export async function mapGenerateConfigToRenderConfig( sessionStorageMountPath: config.sessionStorageMountPath, enableOtel, hasConfigBundle: config.withConfigBundle, + hasFrontend: config.frontend === 'copilotkit', }; } diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index d82d9e808..cea8801ed 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -52,6 +52,7 @@ import { AuthorizerType, Build, Framework, + Frontend, Language, Memory, ModelProvider as ModelProviderEnum, @@ -370,6 +371,7 @@ export class AgentPrimitive extends BasePrimitive { build: 'codezip', agent_type: 'create', network_mode: 'public', + frontend: 'none', has_agent: true, }; expect(COMMAND_SCHEMAS.create.parse(attrs)).toEqual(attrs); @@ -182,6 +183,7 @@ describe('resilientParse', () => { build: 'codezip', agent_type: 'create', network_mode: 'public', + frontend: 'none', has_agent: true, }; expect(resilientParse(COMMAND_SCHEMAS.create, attrs)).toEqual(attrs); diff --git a/src/cli/telemetry/schemas/command-run.ts b/src/cli/telemetry/schemas/command-run.ts index bf79f42eb..02168caeb 100644 --- a/src/cli/telemetry/schemas/command-run.ts +++ b/src/cli/telemetry/schemas/command-run.ts @@ -11,6 +11,7 @@ import { FilterState, FilterType, Framework, + Frontend, GatewayTargetHost, GatewayTargetType, Language, @@ -43,6 +44,7 @@ const CreateAttrs = safeSchema({ build: Build, agent_type: AgentType, network_mode: NetworkMode, + frontend: Frontend, has_agent: z.boolean(), }); @@ -56,6 +58,7 @@ const AddAgentAttrs = safeSchema({ network_mode: NetworkMode, authorizer_type: AuthorizerType, memory: Memory, + frontend: Frontend, }); const AddMemoryAttrs = safeSchema({ diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 4624883cd..e2a8c00b1 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -94,6 +94,7 @@ export const ModelProvider = z.enum(['bedrock', 'anthropic', 'openai', 'gemini'] export const NetworkMode = z.enum(['public', 'vpc']); export const OutboundAuth = z.enum(['oauth', 'api-key', 'none']); export const PolicyEngineMode = z.enum(['log_only', 'enforce']); +export const Frontend = z.enum(['none', 'copilotkit']); export const Protocol = z.enum(['http', 'mcp', 'a2a']); export const RefType = z.enum(['arn', 'name']); export const ResourceType = z.enum(['gateway', 'agent']); diff --git a/src/cli/templates/BaseRenderer.ts b/src/cli/templates/BaseRenderer.ts index 659722926..4ef7855e7 100644 --- a/src/cli/templates/BaseRenderer.ts +++ b/src/cli/templates/BaseRenderer.ts @@ -1,7 +1,9 @@ import { APP_DIR } from '../../lib'; -import { copyAndRenderDir } from './render'; +import { copyAndRenderDir, copyDir } from './render'; import type { AgentRenderConfig } from './types'; +import Handlebars from 'handlebars'; import { existsSync } from 'node:fs'; +import * as fs from 'node:fs/promises'; import * as path from 'node:path'; export interface RendererContext { @@ -65,6 +67,26 @@ export abstract class BaseRenderer { } } + // Render CopilotKit frontend (AGUI with frontend enabled) + // Uses plain copy (not Handlebars) because TSX files contain {{ in JSX syntax. + // Only package.json and index.html need template substitution. + if (this.config.hasFrontend) { + const frontendTemplateDir = path.join(this.baseTemplateDir, 'frontend', 'copilotkit'); + if (existsSync(frontendTemplateDir)) { + const frontendTargetDir = path.join(projectDir, 'frontend'); + await copyDir(frontendTemplateDir, frontendTargetDir); + // Render Handlebars in the files that need variable substitution + for (const file of ['package.json', 'index.html']) { + const filePath = path.join(frontendTargetDir, file); + if (existsSync(filePath)) { + const content = await fs.readFile(filePath, 'utf-8'); + const rendered = Handlebars.compile(content)(templateData); + await fs.writeFile(filePath, rendered, 'utf-8'); + } + } + } + } + // Generate Dockerfile and .dockerignore for Container builds if (this.config.buildType === 'Container') { const language = this.config.targetLanguage.toLowerCase(); diff --git a/src/cli/templates/types.ts b/src/cli/templates/types.ts index 907cc99dd..8f10e57c8 100644 --- a/src/cli/templates/types.ts +++ b/src/cli/templates/types.ts @@ -74,4 +74,6 @@ export interface AgentRenderConfig { enableOtel?: boolean; /** Whether a config bundle is wired into the agent template */ hasConfigBundle?: boolean; + /** Whether to scaffold a CopilotKit frontend (AGUI only) */ + hasFrontend?: boolean; } diff --git a/src/cli/tui/screens/agent/AddAgentScreen.tsx b/src/cli/tui/screens/agent/AddAgentScreen.tsx index c8961c065..5e19d7024 100644 --- a/src/cli/tui/screens/agent/AddAgentScreen.tsx +++ b/src/cli/tui/screens/agent/AddAgentScreen.tsx @@ -175,16 +175,16 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg codeLocation: '', entrypoint: DEFAULT_ENTRYPOINT, buildType: 'CodeZip' as BuildType, - dockerfile: '' as string, + dockerfile: '', modelProvider: 'Bedrock' as ModelProvider, apiKey: undefined as string | undefined, networkMode: 'PUBLIC' as NetworkMode, - subnets: '' as string, - securityGroups: '' as string, - requestHeaderAllowlist: '' as string, - idleTimeout: '' as string, - maxLifetime: '' as string, - sessionStorageMountPath: '' as string, + subnets: '', + securityGroups: '', + requestHeaderAllowlist: '', + idleTimeout: '', + maxLifetime: '', + sessionStorageMountPath: '', withConfigBundle: undefined as boolean | undefined, }); const [byoAdvancedSettings, setByoAdvancedSettings] = useState>(new Set()); @@ -313,6 +313,7 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg maxLifetime: generateWizard.config.maxLifetime, sessionStorageMountPath: generateWizard.config.sessionStorageMountPath, withConfigBundle: generateWizard.config.withConfigBundle, + frontend: generateWizard.config.frontend, pythonVersion: DEFAULT_PYTHON_VERSION, memory: generateWizard.config.memory, }; @@ -490,7 +491,7 @@ export function AddAgentScreen({ existingAgentNames, onComplete, onExit }: AddAg setByoConfig(c => ({ ...c, dockerfile: '', - networkMode: 'PUBLIC' as NetworkMode, + networkMode: 'PUBLIC', subnets: '', securityGroups: '', requestHeaderAllowlist: '', diff --git a/src/cli/tui/screens/agent/types.ts b/src/cli/tui/screens/agent/types.ts index c708bcac0..d0e0638b5 100644 --- a/src/cli/tui/screens/agent/types.ts +++ b/src/cli/tui/screens/agent/types.ts @@ -10,7 +10,7 @@ import type { } from '../../../../schema'; import { DEFAULT_MODEL_IDS, getSupportedModelProviders } from '../../../../schema'; import type { JwtConfigOptions } from '../../../primitives/auth-utils'; -import type { MemoryOption } from '../generate/types'; +import type { FrontendOption, MemoryOption } from '../generate/types'; // ───────────────────────────────────────────────────────────────────────────── // Add Agent Flow Types @@ -57,6 +57,7 @@ export type AddAgentStep = | 'maxLifetime' | 'sessionStorageMountPath' | 'memory' + | 'frontend' | 'region' | 'bedrockAgent' | 'bedrockAlias' @@ -99,6 +100,8 @@ export interface AddAgentConfig { sessionStorageMountPath?: string; /** When true, create a config bundle wired into the agent template */ withConfigBundle?: boolean; + /** Frontend UI to scaffold (AGUI only) */ + frontend?: FrontendOption; /** Python version (only for Python agents) */ pythonVersion: PythonRuntime; /** Memory option (create path only) */ @@ -133,6 +136,7 @@ export const ADD_AGENT_STEP_LABELS: Record = { maxLifetime: 'Max Lifetime', sessionStorageMountPath: 'Session Storage', memory: 'Memory', + frontend: 'Frontend', region: 'Region', bedrockAgent: 'Agent', bedrockAlias: 'Alias', diff --git a/src/cli/tui/screens/agent/useAddAgent.ts b/src/cli/tui/screens/agent/useAddAgent.ts index 2c3539dd4..135dccc3a 100644 --- a/src/cli/tui/screens/agent/useAddAgent.ts +++ b/src/cli/tui/screens/agent/useAddAgent.ts @@ -26,6 +26,7 @@ import { AuthorizerType as AuthorizerTypeEnum, Build, Framework, + Frontend, Language, Memory as MemoryEnum, ModelProvider, @@ -140,6 +141,7 @@ function mapAddAgentConfigToGenerateConfig(config: AddAgentConfig): GenerateConf maxLifetime: config.maxLifetime, sessionStorageMountPath: config.sessionStorageMountPath, withConfigBundle: config.withConfigBundle, + frontend: config.frontend, }; } @@ -169,6 +171,7 @@ export function useAddAgent() { network_mode: standardize(NetworkMode, config.networkMode ?? 'PUBLIC'), authorizer_type: standardize(AuthorizerTypeEnum, config.authorizerType ?? 'NONE'), memory: standardize(MemoryEnum, config.memory ?? 'none'), + frontend: standardize(Frontend, config.frontend ?? 'none'), }, () => addAgentInner(config) ); diff --git a/src/cli/tui/screens/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index cff113610..89bf62bc7 100644 --- a/src/cli/tui/screens/create/useCreateFlow.ts +++ b/src/cli/tui/screens/create/useCreateFlow.ts @@ -28,6 +28,7 @@ import { AgentType, Build, Framework, + Frontend, Language, Memory as MemoryEnum, ModelProvider, @@ -210,6 +211,7 @@ export function useCreateFlow(cwd: string): CreateFlowState { build: standardize(Build, addAgentConfig?.buildType ?? 'CodeZip'), agent_type: standardize(AgentType, addAgentConfig?.agentType ?? 'create'), network_mode: standardize(NetworkMode, addAgentConfig?.networkMode ?? 'PUBLIC'), + frontend: standardize(Frontend, addAgentConfig?.frontend ?? 'none'), has_agent: addAgentConfig !== null, }; @@ -310,6 +312,7 @@ export function useCreateFlow(cwd: string): CreateFlowState { maxLifetime: addAgentConfig.maxLifetime, sessionStorageMountPath: addAgentConfig.sessionStorageMountPath, withConfigBundle: addAgentConfig.withConfigBundle, + frontend: addAgentConfig.frontend, }; logger.logSubStep(`Framework: ${generateConfig.sdk}`); diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index 9c6c79599..4e65e334c 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -22,10 +22,19 @@ import type { SelectableItem } from '../../components'; import { JwtConfigInput, useJwtConfigFlow } from '../../components/jwt-config'; import { useListNavigation, useMultiSelectNavigation } from '../../hooks'; import { RUNTIME_AUTHORIZER_TYPE_OPTIONS } from '../agent/types'; -import type { AdvancedSettingId, BuildType, GenerateConfig, GenerateStep, MemoryOption, ProtocolMode } from './types'; +import type { + AdvancedSettingId, + BuildType, + FrontendOption, + GenerateConfig, + GenerateStep, + MemoryOption, + ProtocolMode, +} from './types'; import { ADVANCED_SETTING_OPTIONS, BUILD_TYPE_OPTIONS, + FRONTEND_OPTIONS, LANGUAGE_OPTIONS, MEMORY_OPTIONS, NETWORK_MODE_OPTIONS, @@ -98,6 +107,8 @@ export function GenerateWizardUI({ })); case 'memory': return MEMORY_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); + case 'frontend': + return FRONTEND_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); case 'networkMode': return NETWORK_MODE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); case 'authorizerType': @@ -147,6 +158,9 @@ export function GenerateWizardUI({ case 'memory': wizard.setMemory(item.id as MemoryOption); break; + case 'frontend': + wizard.setFrontend(item.id as FrontendOption); + break; case 'networkMode': wizard.setNetworkMode(item.id as NetworkMode); break; @@ -577,6 +591,12 @@ function ConfirmView({ config, credentialProjectName }: { config: GenerateConfig {config.sessionStorageMountPath} )} + {config.frontend && config.frontend !== 'none' && ( + + Frontend: + CopilotKit + + )} {config.withConfigBundle && ( Config Bundle: diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index 1b764ea80..b2dc8ab5a 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -20,6 +20,7 @@ export type GenerateStep = | 'modelProvider' | 'apiKey' | 'memory' + | 'frontend' | 'advanced' | 'networkMode' | 'subnets' @@ -34,6 +35,8 @@ export type GenerateStep = export type MemoryOption = 'none' | 'shortTerm' | 'longAndShortTerm'; +export type FrontendOption = 'none' | 'copilotkit'; + // Re-export types from schema for convenience export type { BuildType, ModelProvider, ProtocolMode, SDKFramework, TargetLanguage }; @@ -66,6 +69,8 @@ export interface GenerateConfig { sessionStorageMountPath?: string; /** When true, create a config bundle wired into the agent template */ withConfigBundle?: boolean; + /** Frontend UI to scaffold (AGUI only) */ + frontend?: FrontendOption; } /** Base steps - apiKey, memory, subnets, securityGroups are conditionally added based on selections */ @@ -91,6 +96,7 @@ export const STEP_LABELS: Record = { modelProvider: 'Model', apiKey: 'API Key', memory: 'Memory', + frontend: 'Frontend', advanced: 'Advanced', networkMode: 'Network', subnets: 'Subnets', @@ -212,3 +218,8 @@ export const MEMORY_OPTIONS = [ { id: 'shortTerm', title: 'Short-term memory', description: 'Context within a session' }, { id: 'longAndShortTerm', title: 'Long-term and short-term', description: 'Persists across sessions' }, ] as const; + +export const FRONTEND_OPTIONS = [ + { id: 'copilotkit', title: 'Yes — CopilotKit', description: 'Chat UI with AG-UI streaming' }, + { id: 'none', title: 'No', description: 'No frontend scaffolded' }, +] as const; diff --git a/src/cli/tui/screens/generate/useGenerateWizard.ts b/src/cli/tui/screens/generate/useGenerateWizard.ts index 411cfb151..8d36a921c 100644 --- a/src/cli/tui/screens/generate/useGenerateWizard.ts +++ b/src/cli/tui/screens/generate/useGenerateWizard.ts @@ -1,7 +1,15 @@ import type { NetworkMode, RuntimeAuthorizerType } from '../../../../schema'; import { ProjectNameSchema, SessionStorageSchema } from '../../../../schema'; import type { JwtConfigOptions } from '../../../primitives/auth-utils'; -import type { AdvancedSettingId, BuildType, GenerateConfig, GenerateStep, MemoryOption, ProtocolMode } from './types'; +import type { + AdvancedSettingId, + BuildType, + FrontendOption, + GenerateConfig, + GenerateStep, + MemoryOption, + ProtocolMode, +} from './types'; import { BASE_GENERATE_STEPS, getModelProviderOptionsForSdk } from './types'; import { useCallback, useMemo, useState } from 'react'; @@ -57,6 +65,10 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { const advancedIndex = filtered.indexOf('advanced'); filtered = [...filtered.slice(0, advancedIndex), 'memory', ...filtered.slice(advancedIndex)]; } + if (config.protocol === 'AGUI') { + const advancedIndex = filtered.indexOf('advanced'); + filtered = [...filtered.slice(0, advancedIndex), 'frontend', ...filtered.slice(advancedIndex)]; + } } if (advancedSelected) { const advancedIndex = filtered.indexOf('advanced'); @@ -161,16 +173,17 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { const setModelProvider = useCallback( (modelProvider: GenerateConfig['modelProvider']) => { setConfig(c => ({ ...c, modelProvider })); - // Non-Bedrock providers need API key step if (modelProvider !== 'Bedrock') { setStep('apiKey'); } else if (config.sdk === 'Strands') { setStep('memory'); + } else if (config.protocol === 'AGUI') { + setStep('frontend'); } else { setStep('advanced'); } }, - [config.sdk] + [config.sdk, config.protocol] ); const setApiKey = useCallback( @@ -178,23 +191,39 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { setConfig(c => ({ ...c, apiKey })); if (config.sdk === 'Strands') { setStep('memory'); + } else if (config.protocol === 'AGUI') { + setStep('frontend'); } else { setStep('advanced'); } }, - [config.sdk] + [config.sdk, config.protocol] ); const skipApiKey = useCallback(() => { if (config.sdk === 'Strands') { setStep('memory'); + } else if (config.protocol === 'AGUI') { + setStep('frontend'); } else { setStep('advanced'); } - }, [config.sdk]); + }, [config.sdk, config.protocol]); + + const setMemory = useCallback( + (memory: MemoryOption) => { + setConfig(c => ({ ...c, memory })); + if (config.protocol === 'AGUI') { + setStep('frontend'); + } else { + setStep('advanced'); + } + }, + [config.protocol] + ); - const setMemory = useCallback((memory: MemoryOption) => { - setConfig(c => ({ ...c, memory })); + const setFrontend = useCallback((frontend: FrontendOption) => { + setConfig(c => ({ ...c, frontend })); setStep('advanced'); }, []); @@ -411,6 +440,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { setApiKey, skipApiKey, setMemory, + setFrontend, setAdvanced, advancedSelected, advancedSettings,