Skip to content

Commit bed1de0

Browse files
feat: compile-time feature flag for preview/GA consolidation (#1341)
* feat: implement compile-time feature flag for preview/GA branch consolidation Replace the dual-branch (main/preview) workflow with a single branch using a compile-time __PREVIEW__ constant. esbuild's define block replaces the constant at build time, enabling dead code elimination for GA builds while keeping all harness/preview code in the same source tree. Key changes: - Add src/cli/feature-flags.ts with isPreviewEnabled() wrapper - Configure esbuild define block and vitest define for __PREVIEW__ - Gate harness commands (add tool, remove tool/harness) behind isPreviewEnabled() - Gate harness UI screens and create flow behind the flag - Add globalThis shim in index.ts for tsx dev mode - Update bundle.mjs to produce dual tarballs (GA + preview) - Support ESBUILD_OUTFILE env var for isolated test builds - Port all harness-related source files from preview branch - Add preview-flag.test.ts verifying dead code elimination * fix: remove unnecessary quotes around __PREVIEW__ key in esbuild config Prettier requires unquoted object keys when valid identifiers. * fix: skip harness integration and e2e tests in GA builds Harness features are gated behind BUILD_PREVIEW=1 and eliminated from GA bundles. Integration and e2e tests that exercise harness commands must skip when running against the default (GA) build. * fix: gate harness options in invoke/create commands and fix remove memory cleanup - Wrap harness-related CLI options in invoke command behind isPreviewEnabled() so they don't leak into GA build's --help output - Wrap harness-related CLI options in create command behind isPreviewEnabled() - Fix remove harness leaving orphaned memory entries in agentcore.json - Fix deploy preflight rejecting harness-only projects - Add integration test for harness re-add after removal * fix: auto-deploy harness in TUI dev mode before invoking The TUI path for `agentcore dev` skipped deployment, going straight to the invoke screen. This caused "No deployed targets found" errors for users who hadn't manually run `agentcore deploy`. Now uses the existing useDevDeploy hook to deploy before transitioning to harness invoke mode. * fix: use queueMicrotask for deploy-to-harness transition in TUI dev The derived effectiveMode approach didn't trigger re-renders, leaving the deploy screen stuck after completion. Switch to queueMicrotask + setMode (matching the preview branch pattern) so the transition fires correctly. Also handles browser mode by calling onLaunchBrowser after deploy. * fix: match preview branch deploy UI and persist deployHash - Deploy screen in TUI dev mode now matches preview branch: shows "Deploying project resources..." with DeployStatus CFN messages, filters redundant step, yellow error text, and log path link. - Persist deployHash in deployed-state.json after successful deploys so canSkipDeploy can detect unchanged projects and skip re-deploy. * fix: align harness UX with preview branch - InvokeScreen: Ctrl+N for new session (was bare N), hint messages rendered in gray, context-sensitive "Loading..."/"Thinking..." label, directional scroll arrows instead of numeric range - DevScreen: disable keyboard input while exiting (!isExiting guard) - deploy/actions: imperative harness teardown before stack destroy (gated behind isPreviewEnabled) so harnesses aren't orphaned - browser-mode: resolve harness traces via resolveAgentOrHarness instead of ignoring harnessName parameter - resolve-agent: add resolveHarness and resolveAgentOrHarness helpers * fix: hint message render order and harness/runtime disambiguation - isHint check now comes after isExec (matching preview branch) so exec messages always render as magenta, not gray - When a project has both runtimes and harnesses and no flag is given, show a clear error listing both --runtime and --harness options * fix: route harness invocations and show deploy box in dev mode The browser in `agentcore dev` could not invoke harnesses because the /invocations POST handler never checked for harnessName in the request body. Add early routing to handleHarnessInvocation when present. The deploy progress box was missing because useDevDeploy did not pass verbose: true or onResourceEvent to handleDeploy, so the switchableIoHost was never created and CloudFormation messages never reached the TUI. Constraint: handleHarnessInvocation handler already existed but was unreachable Rejected: Adding a separate /harness-invocations endpoint | breaks frontend contract Confidence: high Scope-risk: narrow * fix: serve harness info in status API and fix deploy message flow The browser frontend could not discover harnesses because /api/status did not include harness data or selectedHarness in its response. The deploy box was still not showing because switchableIoHost was only created with verbose:true, but preview also creates it when onDeployMessage is provided. Also wire onDeployMessage into the setOnMessage callback so both callbacks receive deploy events. Constraint: frontend polls /api/status to discover available targets Rejected: Separate /api/harnesses endpoint | adds complexity for no benefit Confidence: high Scope-risk: narrow * fix: include harnesses in /api/resources response for browser UI The frontend Agent Inspector crashes with "Cannot read properties of undefined (reading 'find')" when switching to a harness because the /api/resources endpoint was missing the harnesses array entirely. Constraint: Frontend expects harnesses array with deploymentStatus and deployed fields Confidence: high Scope-risk: narrow * test: add unit tests for harness invocation and status handlers Covers validation, routing, SSE streaming, error handling, and the harness fields added to /api/status response. Confidence: high Scope-risk: narrow * test: add unit tests for resolve-agent, change-detection, and harness-mapper Cover the untested middle-layer logic that routes invocations, decides whether deploys can be skipped, and maps harness specs to API payloads. Confidence: high Scope-risk: narrow * fix: adjust preview-flag DCE test for keepNames compatibility The keepNames:true esbuild option (added for better error stacks) preserves class/function name strings even in dead code paths. Adjust assertions to check for harness-only module markers that are fully tree-shaken, rather than name strings that survive. Confidence: high Scope-risk: narrow * fix: pass structured DeployMessage through to TUI instead of plain strings The dev deploy hook was re-wrapping string messages into DeployMessage objects with hardcoded values. Now the real message (with actual level, code, timestamp) flows through directly from CDK. * fix(harness): populate retrievalConfig from memory strategy namespaces Includes EPISODIC reflectionNamespaces in the retrieval config so the harness runtime searches all relevant memory namespaces at inference time. Also incorporates memorySpec into the deploy hash so namespace changes trigger a harness update. Cherry-picked from #1374. * fix: rewrite dev command to resolve merge conflict with telemetry refactor The merge conflict resolution left duplicated code blocks and mismatched braces. Rebuilt from main's withCommandRunTelemetry+recorder pattern and re-applied preview feature flag gates (harness support, skip-deploy, TUI picker). * fix: address PR review feedback from jariy17 - Remove unnecessary `as unknown as` cast in agentcore-control.ts (SDK already has IndexedKey type) - Optimize preview build to only re-run esbuild (not full tsc + assets) - Delete dead code: poll.ts and its test (nothing imports it) - Error when harness enters FAILED state after create/update instead of silently returning success - Add unit tests for harness-deployer (create, update, skip, teardown, FAILED status, role resolution, retry, polling) * fix: update release workflows for single-branch preview model - release.yml: set BUILD_PREVIEW=1 for preview builds - release-main-and-preview.yml: rewrite as single-PR flow using preview-version.json for the preview version track (no separate preview branch needed) - Remove sync-preview.yml (no preview branch to sync) - Update preview-version.json to match current npm state (1.0.0-preview.9) Constraint: preview and GA are both built from main, differentiated by BUILD_PREVIEW env var at esbuild time Confidence: high Scope-risk: narrow * feat: add release_target input to select main, preview, or both Allows running the release workflow for just main-only, preview-only, or both together. Jobs are conditionally skipped based on the selection. Confidence: high Scope-risk: narrow * fix: use github.ref_name for release branch selection Allows triggering the release workflow from any branch via the GitHub UI branch selector, instead of hardcoding main. Confidence: high Scope-risk: narrow * chore: remove unused BUILD_PREVIEW env from release.yml Preview releases are handled by release-main-and-preview.yml. Confidence: high Scope-risk: narrow
1 parent 69b4ce6 commit bed1de0

122 files changed

Lines changed: 11983 additions & 712 deletions

File tree

Some content is hidden

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

.github/workflows/release-main-and-preview.yml

Lines changed: 174 additions & 202 deletions
Large diffs are not rendered by default.

.github/workflows/sync-preview.yml

Lines changed: 0 additions & 192 deletions
This file was deleted.

e2e-tests/harness-bedrock.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createHarnessE2ESuite } from './harness-e2e-helper.js';
2+
3+
createHarnessE2ESuite({ modelProvider: 'bedrock' });

e2e-tests/harness-e2e-helper.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { hasAwsCredentials, parseJsonOutput, prereqs, retry, spawnAndCollect } from '../src/test-utils/index.js';
2+
import {
3+
cleanupStaleCredentialProviders,
4+
installCdkTarball,
5+
runAgentCoreCLI,
6+
teardownE2EProject,
7+
writeAwsTargets,
8+
} from './e2e-helper.js';
9+
import { randomUUID } from 'node:crypto';
10+
import { mkdir, rm } from 'node:fs/promises';
11+
import { tmpdir } from 'node:os';
12+
import { join } from 'node:path';
13+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
14+
15+
const hasAws = hasAwsCredentials();
16+
// Harness features are only available in preview builds (BUILD_PREVIEW=1).
17+
const isPreviewBuild = process.env.BUILD_PREVIEW === '1';
18+
const baseCanRun = prereqs.npm && prereqs.git && hasAws && isPreviewBuild;
19+
20+
interface HarnessE2EConfig {
21+
modelProvider: 'bedrock' | 'open_ai' | 'gemini';
22+
requiredEnvVar?: string;
23+
skipMemory?: boolean;
24+
}
25+
26+
export function createHarnessE2ESuite(cfg: HarnessE2EConfig) {
27+
const hasRequiredVar = !cfg.requiredEnvVar || !!process.env[cfg.requiredEnvVar];
28+
const canRun = baseCanRun && hasRequiredVar;
29+
30+
const providerLabel =
31+
cfg.modelProvider === 'open_ai' ? 'OpenAI' : cfg.modelProvider === 'gemini' ? 'Gemini' : 'Bedrock';
32+
33+
describe.sequential(`e2e: harness/${providerLabel} — create → deploy → invoke`, () => {
34+
let testDir: string;
35+
let projectPath: string;
36+
let harnessName: string;
37+
38+
beforeAll(async () => {
39+
if (!canRun) return;
40+
41+
await cleanupStaleCredentialProviders();
42+
43+
testDir = join(tmpdir(), `agentcore-e2e-harness-${randomUUID()}`);
44+
await mkdir(testDir, { recursive: true });
45+
46+
const providerSlug = cfg.modelProvider.replace('_', '').slice(0, 4);
47+
harnessName = `E2eHrns${providerSlug}${String(Date.now()).slice(-8)}`;
48+
49+
const createArgs = [
50+
'create',
51+
'--name',
52+
harnessName,
53+
'--model-provider',
54+
cfg.modelProvider,
55+
'--json',
56+
'--skip-git',
57+
];
58+
59+
if (cfg.requiredEnvVar && process.env[cfg.requiredEnvVar]) {
60+
createArgs.push('--api-key-arn', process.env[cfg.requiredEnvVar]!);
61+
}
62+
63+
if (cfg.skipMemory) {
64+
createArgs.push('--no-harness-memory');
65+
}
66+
67+
const result = await runAgentCoreCLI(createArgs, testDir);
68+
69+
expect(result.exitCode, `Create failed: ${result.stderr}`).toBe(0);
70+
const json = parseJsonOutput(result.stdout) as { projectPath: string };
71+
projectPath = json.projectPath;
72+
73+
await writeAwsTargets(projectPath);
74+
installCdkTarball(projectPath);
75+
}, 300000);
76+
77+
afterAll(async () => {
78+
if (projectPath && hasAws) {
79+
await teardownE2EProject(projectPath, harnessName, cfg.modelProvider);
80+
}
81+
if (testDir) await rm(testDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 1000 });
82+
}, 600000);
83+
84+
it.skipIf(!canRun)(
85+
'deploys to AWS successfully',
86+
async () => {
87+
expect(projectPath, 'Project should have been created').toBeTruthy();
88+
89+
await retry(
90+
async () => {
91+
const result = await runAgentCoreCLI(['deploy', '--yes', '--json'], projectPath);
92+
93+
if (result.exitCode !== 0) {
94+
console.log('Deploy stdout:', result.stdout);
95+
console.log('Deploy stderr:', result.stderr);
96+
}
97+
98+
expect(result.exitCode, `Deploy failed (stderr: ${result.stderr}, stdout: ${result.stdout})`).toBe(0);
99+
100+
const json = parseJsonOutput(result.stdout) as { success: boolean };
101+
expect(json.success, 'Deploy should report success').toBe(true);
102+
},
103+
1,
104+
30000
105+
);
106+
},
107+
600000
108+
);
109+
110+
it.skipIf(!canRun)(
111+
'invokes the deployed harness',
112+
async () => {
113+
expect(projectPath, 'Project should have been created').toBeTruthy();
114+
115+
await retry(
116+
async () => {
117+
const result = await runAgentCoreCLI(
118+
['invoke', '--harness', harnessName, '--prompt', 'Say hello', '--json'],
119+
projectPath
120+
);
121+
122+
if (result.exitCode !== 0) {
123+
console.log('Invoke stdout:', result.stdout);
124+
console.log('Invoke stderr:', result.stderr);
125+
}
126+
127+
expect(result.exitCode, `Invoke failed: ${result.stderr}`).toBe(0);
128+
129+
const json = parseJsonOutput(result.stdout) as { success: boolean };
130+
expect(json.success, 'Invoke should report success').toBe(true);
131+
},
132+
3,
133+
15000
134+
);
135+
},
136+
180000
137+
);
138+
139+
it.skipIf(!canRun)(
140+
'status shows the deployed harness',
141+
async () => {
142+
const statusResult = await spawnAndCollect('agentcore', ['status', '--json'], projectPath);
143+
144+
expect(statusResult.exitCode, `Status failed: ${statusResult.stderr}`).toBe(0);
145+
146+
const json = parseJsonOutput(statusResult.stdout) as {
147+
success: boolean;
148+
resources: {
149+
resourceType: string;
150+
name: string;
151+
deploymentState: string;
152+
identifier?: string;
153+
}[];
154+
};
155+
expect(json.success).toBe(true);
156+
157+
const harness = json.resources.find(r => r.resourceType === 'harness' && r.name === harnessName);
158+
expect(harness, `Harness "${harnessName}" should appear in status`).toBeDefined();
159+
expect(harness!.deploymentState).toBe('deployed');
160+
expect(harness!.identifier, 'Deployed harness should have a harnessArn').toBeTruthy();
161+
},
162+
120000
163+
);
164+
});
165+
}

e2e-tests/harness-gemini.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createHarnessE2ESuite } from './harness-e2e-helper.js';
2+
3+
createHarnessE2ESuite({ modelProvider: 'gemini', requiredEnvVar: 'GEMINI_API_KEY_ARN', skipMemory: true });

e2e-tests/harness-openai.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createHarnessE2ESuite } from './harness-e2e-helper.js';
2+
3+
createHarnessE2ESuite({ modelProvider: 'open_ai', requiredEnvVar: 'OPENAI_API_KEY_ARN', skipMemory: true });

0 commit comments

Comments
 (0)