Skip to content

Commit 0214f86

Browse files
authored
feat: modular primitive architecture (#481)
* feat: implement modular primitive architecture Refactor all resource types (agent, memory, credential, gateway, gateway-target) into self-contained primitive classes that own the full add/remove lifecycle. Each primitive extends BasePrimitive and implements add(), remove(), previewRemove(), getRemovable(), and registerCommands(). Primitives absorb scattered operation functions and register their own CLI subcommands via a registry loop. * fix: update primitives for gateway integration and resolve post-rebase issues Absorb gateway integration changes from main into the primitive architecture: - Update GatewayPrimitive with unassigned targets, OAuth auto-credential, allowedScopes - Update GatewayTargetPrimitive with mcp-tool→gateway-target rename and external endpoints - Update CredentialPrimitive with OAuth2 discriminated union, managed credential protection, gateway target reference checking, and hyphen→underscore env var fix - Remove VPC-related code from AgentPrimitive (removed upstream) - Break circular dependency: validate.ts no longer imports from primitive registry - Delete obsolete test files for absorbed modules, fix test mocks and expectations * chore: remove RefactorTest directory Test project accidentally included in the primitive architecture commit. * fix: correct import ordering in schema-mapper.ts for prettier * fix: address PR review feedback - type safety and naming consistency - Replace `any` with `unknown`/`RemovableResource` in registry.ts - Remove unused `computeDefaultIdentityEnvVarName` alias from CredentialPrimitive and credential-utils * fix: wire gateway primitives to actual CLI logic and fix remove-all - GatewayPrimitive.registerCommands now calls validation and add/remove logic with proper JSON output instead of "coming soon" stubs - GatewayTargetPrimitive.registerCommands wired similarly, supporting both existing-endpoint and create-new flows - handleRemoveAll now resets mcp.json gateways alongside agentcore.json
1 parent 7302109 commit 0214f86

75 files changed

Lines changed: 3443 additions & 3337 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.

AGENTS.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ src/
1010
├── schema/ # Schema definitions with Zod validators
1111
├── lib/ # Shared utilities (ConfigIO, packaging)
1212
├── cli/ # CLI implementation
13-
│ ├── commands/ # CLI commands
13+
│ ├── primitives/ # Resource primitives (add/remove logic per resource type)
14+
│ ├── commands/ # CLI commands (thin Commander registration)
1415
│ ├── tui/ # Terminal UI (Ink/React)
15-
│ ├── operations/ # Business logic
16+
│ ├── operations/ # Shared business logic (schema mapping, deploy, etc.)
1617
│ ├── cdk/ # CDK toolkit wrapper for programmatic CDK operations
1718
│ └── templates/ # Project templating
1819
└── assets/ # Template assets vended to users
@@ -50,6 +51,25 @@ Note: CDK L3 constructs are in a separate package `@aws/agentcore-cdk`.
5051

5152
- MCP gateway and tool support (`add gateway`, `add mcp-tool`) - currently hidden
5253

54+
## Primitives Architecture
55+
56+
All resource types (agent, memory, identity, gateway, mcp-tool) are modeled as **primitives** — self-contained classes
57+
in `src/cli/primitives/` that own the full add/remove lifecycle for their resource type.
58+
59+
Each primitive extends `BasePrimitive` and implements: `add()`, `remove()`, `previewRemove()`, `getRemovable()`,
60+
`registerCommands()`, and `addScreen()`.
61+
62+
Current primitives:
63+
64+
- `AgentPrimitive` — agent creation (template + BYO), removal, credential resolution
65+
- `MemoryPrimitive` — memory creation with strategies, removal
66+
- `CredentialPrimitive` — credential/identity creation, .env management, removal
67+
- `GatewayPrimitive` — MCP gateway creation/removal (hidden, coming soon)
68+
- `GatewayTargetPrimitive` — MCP tool creation/removal with code generation (hidden, coming soon)
69+
70+
Singletons are created in `registry.ts` and wired into CLI commands via `cli.ts`. See `src/cli/AGENTS.md` for details on
71+
adding new primitives.
72+
5373
## Vended CDK Project
5474

5575
When users run `agentcore create`, we vend a CDK project at `agentcore/cdk/` that:
@@ -88,3 +108,16 @@ See `docs/TESTING.md` for details.
88108
## Related Package
89109

90110
- `@aws/agentcore-cdk` - CDK constructs used by vended projects
111+
112+
## Code Style
113+
114+
- Never use inline imports. Imports must always go at the top of the file.
115+
- Wheverever there is a requirement to use something that returns a success result and an error message you must use
116+
this format
117+
118+
```javascript
119+
{ success: Boolean, error?:string}
120+
```
121+
122+
- Always look for existing types before creating a new type inline.
123+
- Re-usable constants must be defined in a constants file in the closest sensible subdirectory.

src/cli/AGENTS.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,107 @@ The `dev` command uses a strategy pattern with a `DevServer` base class and two
6464

6565
The server selection is based on `agent.build` (`CodeZip` or `Container`).
6666

67+
## Primitives Architecture
68+
69+
All resource types are modeled as **primitives** in `primitives/`. Each primitive is a self-contained class that owns
70+
the full add/remove lifecycle for one resource type. CLI commands and TUI flows consume primitives polymorphically.
71+
72+
### Directory Structure
73+
74+
```
75+
primitives/
76+
├── BasePrimitive.ts # Abstract base class with shared helpers
77+
├── AgentPrimitive.tsx # Agent add/remove (template + BYO paths)
78+
├── MemoryPrimitive.tsx # Memory add/remove
79+
├── CredentialPrimitive.tsx # Credential/identity add/remove + .env management
80+
├── GatewayPrimitive.ts # MCP gateway add/remove (hidden, coming soon)
81+
├── GatewayTargetPrimitive.ts # MCP tool add/remove + code gen (hidden, coming soon)
82+
├── registry.ts # Singleton instances + ALL_PRIMITIVES array
83+
├── credential-utils.ts # Shared credential env var name computation
84+
├── constants.ts # SOURCE_CODE_NOTE and other shared constants
85+
├── types.ts # AddResult, RemovableResource, RemovalResult, etc.
86+
└── index.ts # Barrel exports
87+
```
88+
89+
### BasePrimitive Contract
90+
91+
Every primitive extends `BasePrimitive<TAddOptions, TRemovable>` and implements:
92+
93+
- `kind` — resource identifier (`'agent'`, `'memory'`, `'identity'`, `'gateway'`, `'mcp-tool'`)
94+
- `label` — human-readable name (`'Agent'`, `'Memory'`, `'Identity'`)
95+
- `add(options)` — create a resource, returns `AddResult`
96+
- `remove(name)` — remove a resource, returns `RemovalResult`
97+
- `previewRemove(name)` — preview what removal will do
98+
- `getRemovable()` — list resources available for removal
99+
- `registerCommands(addCmd, removeCmd)` — register CLI subcommands
100+
101+
BasePrimitive provides shared helpers:
102+
103+
- `configIO` — shared ConfigIO instance for agentcore.json
104+
- `readProjectSpec()` / `writeProjectSpec()` — read/write agentcore.json
105+
- `checkDuplicate()` — validate name uniqueness
106+
- `article` — indefinite article for grammar (`'a'` or `'an'`)
107+
- `registerRemoveSubcommand(removeCmd)` — standard remove CLI handler (CLI mode + TUI fallback)
108+
109+
### Adding a New Primitive
110+
111+
1. Create `src/cli/primitives/NewPrimitive.ts` extending `BasePrimitive`
112+
2. Implement all abstract methods (`add`, `remove`, `previewRemove`, `getRemovable`, `registerCommands`, `addScreen`)
113+
3. Add a singleton to `registry.ts` and include it in `ALL_PRIMITIVES`
114+
4. Export from `index.ts`
115+
5. The primitive auto-registers its CLI subcommands via the loop in `cli.ts`
116+
117+
### Key Design Rules
118+
119+
- **Absorb, don't wrap.** Each primitive owns its logic directly. Do not create facade files that delegate to
120+
primitives.
121+
- **No backward-compatibility shims.** This is a CLI, not a library. If the CLI functions the same, delete old files.
122+
- **Use `{ success, error? }` result format** throughout (never `{ ok, error }`). See `AddResult` and `RemovalResult`.
123+
- **Dynamic imports for ink/React only.** TUI components (ink, react, screen components) must be dynamically imported
124+
inside Commander action handlers to prevent esbuild async module propagation issues. All other imports go at the top
125+
of the file. See the esbuild section below.
126+
127+
### esbuild Async Module Constraint
128+
129+
ink uses top-level `await` (via yoga-wasm). Any module that imports ink at the top level becomes async in esbuild's ESM
130+
bundle. If the async propagation fails (e.g., through circular dependencies), esbuild generates `await` inside non-async
131+
functions, causing a runtime `SyntaxError`. To prevent this:
132+
133+
- **Never import ink, react, or TUI screen components at the top of primitive files.**
134+
- Use `await Promise.all([import('ink'), import('react'), import('...')])` inside Commander `.action()` handlers.
135+
- This is the one exception to the "no inline imports" rule in the root AGENTS.md.
136+
- `registry.ts` imports all primitive classes — if any primitive pulls in ink at the top level, all modules that import
137+
from registry become async, causing cascading failures.
138+
139+
### Registry and Wiring
140+
141+
`registry.ts` creates singleton instances of all primitives:
142+
143+
```typescript
144+
export const agentPrimitive = new AgentPrimitive();
145+
export const memoryPrimitive = new MemoryPrimitive();
146+
// ...
147+
export const ALL_PRIMITIVES = [agentPrimitive, memoryPrimitive, ...];
148+
```
149+
150+
`cli.ts` wires them into Commander:
151+
152+
```typescript
153+
for (const primitive of ALL_PRIMITIVES) {
154+
primitive.registerCommands(addCmd, removeCmd);
155+
}
156+
```
157+
158+
### TUI Hooks
159+
160+
TUI remove hooks in `tui/hooks/useRemove.ts` use generic helpers:
161+
162+
- `useRemovableResources<T>(loader)` — generic hook for loading removable resources from any primitive
163+
- `useRemoveResource<TIdentifier>(removeFn, resourceType, getName)` — generic hook for removing any resource with
164+
logging
165+
166+
Each resource-specific hook (e.g., `useRemovableAgents`, `useRemoveMemory`) is a thin wrapper around the generic.
167+
67168
## Commands Directory Structure
68169

69170
Commands live in `commands/`. Each command has its own directory with an `index.ts` barrel file and a file called

src/cli/cli.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { registerStatus } from './commands/status';
1111
import { registerUpdate } from './commands/update';
1212
import { registerValidate } from './commands/validate';
1313
import { PACKAGE_VERSION } from './constants';
14+
import { ALL_PRIMITIVES } from './primitives';
1415
import { App } from './tui/App';
1516
import { LayoutProvider } from './tui/context';
1617
import { COMMAND_DESCRIPTIONS } from './tui/copy';
@@ -124,18 +125,23 @@ export function createProgram(): Command {
124125
}
125126

126127
export function registerCommands(program: Command) {
127-
registerAdd(program);
128+
const addCmd = registerAdd(program);
128129
registerDev(program);
129130
registerDeploy(program);
130131
registerCreate(program);
131132
registerHelp(program);
132133
registerInvoke(program);
133134
registerLogs(program);
134135
registerPackage(program);
135-
registerRemove(program);
136+
const removeCmd = registerRemove(program);
136137
registerStatus(program);
137138
registerUpdate(program);
138139
registerValidate(program);
140+
141+
// Register primitive subcommands (add agent, remove agent, add memory, etc.)
142+
for (const primitive of ALL_PRIMITIVES) {
143+
primitive.registerCommands(addCmd, removeCmd);
144+
}
139145
}
140146

141147
export const main = async (argv: string[]) => {

src/cli/commands/AGENTS.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,22 @@ If removing a command file would remove behavior, the design is wrong.
3030
- Commander registration only
3131
- Parses CLI args and invokes handler or TUI
3232

33-
2. **Action File** (`commands/{name}/action.ts`)
34-
- Contains business logic specific to this command
33+
2. **Primitives** (`primitives/`) — for resource add/remove commands
34+
- Each resource type (agent, memory, identity, gateway, mcp-tool) has a primitive class
35+
- Primitives own add/remove logic, CLI subcommand registration, and TUI screen routing
36+
- `add` and `remove` commands delegate to primitives via `primitive.registerCommands()`
37+
- See `src/cli/AGENTS.md` for the full primitives architecture
38+
39+
3. **Action File** (`commands/{name}/action.ts`) — for non-resource commands
40+
- Contains business logic specific to this command (e.g., deploy, invoke, package)
3541
- Defines interfaces (e.g., `InvokeContext`, `PackageResult`)
3642
- Implements handlers (e.g., `handleInvoke`, `loadPackageConfig`)
3743
- Must be UI-agnostic where possible
3844

39-
3. **Operation** (`operations/{domain}/`)
40-
- Own all shared decisions, sequencing, validation, side effects
45+
4. **Operation** (`operations/{domain}/`)
46+
- Shared utilities consumed by primitives and commands (schema mapping, template rendering)
4147
- Must be UI-agnostic (no Ink, no process.exit)
42-
- Reusable by TUI screens and command handlers alike
48+
- Reusable by TUI screens, primitives, and command handlers alike
4349

4450
## Good vs Bad Examples
4551

src/cli/commands/add/__tests__/actions.test.ts

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

0 commit comments

Comments
 (0)