Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1fac16f
feat(project): resolve deployed invoke resources
aidandaly24 Aug 25, 2026
1064873
refactor(project): resolve resources from deployed stack state
aidandaly24 Aug 27, 2026
73ab93d
refactor(project): remove duplicate deployment reader
aidandaly24 Aug 27, 2026
88b2b54
test(project): seed deployed state through shared helper
aidandaly24 Aug 27, 2026
eafb214
refactor(invoke): share Runtime and Harness operations
aidandaly24 Aug 25, 2026
0ec94aa
refactor(invoke): support embedded invoke consoles
aidandaly24 Aug 27, 2026
7c70cfe
feat(project): add Runtime and Harness invoke commands
aidandaly24 Aug 28, 2026
30c2c3f
feat(project): add invoke resource picker
aidandaly24 Aug 28, 2026
5ed201c
feat(project): register invoke commands
aidandaly24 Aug 28, 2026
be1574e
docs(project): document project-aware invoke
aidandaly24 Aug 28, 2026
c68df2a
docs(templates): document deployed Runtime invoke
aidandaly24 Aug 28, 2026
f139810
test(project): inline deployed state setup
aidandaly24 Aug 28, 2026
df3f9c1
refactor(project): clarify deployed resource lookup
aidandaly24 Aug 28, 2026
bfebe6f
refactor(project): clarify target credential helper
aidandaly24 Aug 28, 2026
0ec27ca
test(project): remove redundant resource resolution tests
aidandaly24 Aug 28, 2026
4d83bc8
test(project): recognize invoke as an implemented screen
aidandaly24 Aug 31, 2026
251c965
fix(project): resolve project for invoke menu
aidandaly24 Aug 31, 2026
a295a83
refactor(project): resolve resources through one stack read
aidandaly24 Aug 31, 2026
c198bd4
feat(project): expose deployed project resources
aidandaly24 Aug 31, 2026
d1f8033
test(project): update backend doubles for bulk resolution
aidandaly24 Aug 31, 2026
de9c968
fix(project): show only deployed invoke resources
aidandaly24 Aug 31, 2026
fde1e27
fix(project): return from invoke picker to project menu
aidandaly24 Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ agentcore # interactive TUI
│ │ # payment-manager, payment-connector — or `all`, which
│ │ # empties every resource collection (y/N prompt; --yes
│ │ # skips it for non-interactive use)
│ ├── build # synthesize the project's CloudFormation templates
│ ├── dev # run the project locally
│ ├── deploy # deploy to AWS (auto-provisions the default target)
│ └── dev # run the project's agents locally
│ ├── invoke # invoke a deployed project resource
│ │ ├── runtime # use the existing Runtime invoke experience
│ │ └── harness # use the existing Harness invoke experience
│ ├── status # inspect deployed project resources
│ └── build # synthesize the project's CloudFormation templates
└── config # read/write global config values
```

Expand All @@ -151,6 +155,26 @@ Global flags (declared at the root, available on every command):
| `--debug` | Debug logging. |
| `--endpoint-url` | Override the service endpoint URL (e.g. for testing against a stub). |

### Invoke a project resource

Run `agentcore project invoke` from inside a project to choose a deployed
Runtime or Harness interactively. Headless invocation keeps each resource's
existing input contract:

```bash
agentcore project invoke runtime \
--name checkout \
--payload '{"prompt":"Check order 123."}' \
--content-type application/json

agentcore project invoke harness \
--name support \
--prompt "Help with my account."
```

Use `--target` to select a deployment target. When a project declares exactly
one resource of the requested type, `--name` may be omitted.

### Examples

```bash
Expand Down
6 changes: 6 additions & 0 deletions src/assets/templates/hello-world-python-container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ Environment variables for local development go in `agentcore/.env.local`
```bash
agentcore project deploy
```

Invoke the deployed Runtime with its native payload:

```bash
agentcore project invoke runtime --payload '{"prompt":"Hello!"}'
```
8 changes: 5 additions & 3 deletions src/assets/templates/hello-world-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ curl -X POST http://localhost:8080/invocations \
-d '{"prompt": "Hello!"}'
```

<!-- TODO: replace the uv run + curl instructions with `agentcore dev` and
`agentcore invoke` once those commands are available. -->

## Build your agent

Start in `main.py`:
Expand Down Expand Up @@ -58,3 +55,8 @@ for multi-agent patterns, MCP tools, and model configuration.

Deploy from the project root with the AgentCore CLI; the CDK app under
`agentcore/cdk` provisions the Runtime that hosts this agent.

```bash
agentcore project deploy
agentcore project invoke runtime --payload '{"prompt":"Hello!"}'
```
6 changes: 6 additions & 0 deletions src/assets/templates/strands-http-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,9 @@ Command Prompt, or `.\.venv\Scripts\activate.ps1` in Windows PowerShell.
# Deployment

After providing credentials, `agentcore project deploy` will deploy your project into Amazon Bedrock AgentCore.

Invoke the deployed Runtime with its native payload:

```bash
agentcore project invoke runtime --payload '{"prompt":"Hello!"}'
```
5 changes: 5 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import { GatewayRuleGetScreen } from "../handlers/gateway/rule/get/screen.tsx";
import { GatewayInvokeScreen } from "../handlers/gateway/invoke/screen.tsx";
import { ProjectScreen, ProjectCommandNotImplementedScreen } from "../handlers/project/screen.tsx";
import { ProjectCreateScreen } from "../handlers/project/create/screen.tsx";
import { ProjectInvokePickerScreen } from "../handlers/project/invoke/screen.tsx";
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
import type { Context } from "../router";

Expand Down Expand Up @@ -147,6 +148,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path="agentcore" element={<RootScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/project/invoke"
element={<ProjectInvokePickerScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/harness" element={<HarnessScreen ctx={ctx} core={core} />} />
{/* Bare `get` (no id) has nothing to show — send the user to the list. */}
<Route
Expand Down
157 changes: 145 additions & 12 deletions src/core/project/backends/cdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { existsSync } from "node:fs";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import type { Stack } from "@aws-sdk/client-cloudformation";
import type { DeployResult, Project, ProjectEvent } from "../../../handlers/project/types";
import { FsReadWriteJson } from "../../../io";
import { ProjectSpecSchema } from "../../../projectSchemas/project";
import { createSilentLogger } from "../../../testing";
import { CdkBackend } from "./cdk";
import { DEPLOYED_STATE_RELATIVE_PATH } from "./cdk/deployedState";
import { DEPLOYED_STATE_RELATIVE_PATH, updateTargetState } from "./cdk/deployedState";
import type { DeployBackendInput } from "./types";
import type { BootstrapState } from "./cdk/environment";
import type { CdkCredentialProvider, CdkOperation, CdkOutputs, CdkRunOptions } from "./cdk/toolkit";
Expand All @@ -17,6 +19,9 @@ const TARGET = {
account: "111122223333",
region: "us-east-1",
} as const;
const STACK_ARN =
"arn:aws:cloudformation:us-east-1:111122223333:stack/AgentCore-example-default/abc";
const json = new FsReadWriteJson({ logger: createSilentLogger() });

/** A template holding only what CDK adds itself, as an empty project synthesizes. */
const METADATA_ONLY = { CDKMetadata: { Type: "AWS::CDK::Metadata" } };
Expand Down Expand Up @@ -120,8 +125,8 @@ type HarnessOptions = {
template?: boolean;
failOperation?: CdkOperation["kind"];
bootstrapError?: Error;
/** Whether CloudFormation still holds the target's stack. Defaults to present. */
stackExists?: boolean;
/** Stack returned by CloudFormation. Defaults to a present stack; null means absent. */
describedStack?: Stack | null;
};

function harness(options: HarnessOptions = {}) {
Expand All @@ -132,7 +137,8 @@ function harness(options: HarnessOptions = {}) {
const bootstrapCredentials: CdkCredentialProvider[] = [];
const accountRegions: string[] = [];
const bootstrapRegions: string[] = [];
const stackProbes: string[] = [];
const stackReads: { stackName: string; region: string; credentials: CdkCredentialProvider }[] =
[];
let templateLoads = 0;
let templateCleanups = 0;
const credentials: CdkCredentialProvider = async () => ({
Expand Down Expand Up @@ -161,10 +167,6 @@ function harness(options: HarnessOptions = {}) {
if (options.bootstrapError) throw options.bootstrapError;
return options.bootstrap ?? { kind: "current", version: 30 };
},
stack: async (stackName) => {
stackProbes.push(stackName);
return options.stackExists ?? true;
},
cdk: async (operation, runOptions) => {
runs.push({ operation, options: runOptions });
if (operation.kind === options.failOperation) {
Expand Down Expand Up @@ -194,6 +196,17 @@ function harness(options: HarnessOptions = {}) {
},
};
},
describeStack: async (region, provider, stackName) => {
stackReads.push({ stackName, region, credentials: provider });
if (options.describedStack === null) return undefined;
return (
options.describedStack ?? {
StackName: stackName,
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
}
);
},
});

return {
Expand All @@ -206,7 +219,7 @@ function harness(options: HarnessOptions = {}) {
credentialRegions,
credentials,
runs,
stackProbes,
stackReads,
templateLoads: () => templateLoads,
templateCleanups: () => templateCleanups,
};
Expand Down Expand Up @@ -434,7 +447,13 @@ describe("CdkBackend.deploy", () => {
expect(subject.runs.map(({ operation }) => operation)).toEqual([
{ kind: "destroy", stackArtifactId: "AgentCore-example-default-0" },
]);
expect(subject.stackProbes).toEqual(["AgentCore-example-default-0"]);
expect(subject.stackReads).toEqual([
{
stackName: "AgentCore-example-default-0",
region: TARGET.region,
credentials: subject.credentials,
},
]);
expect(JSON.parse(await Bun.file(statePath).text())).toEqual({
targets: { prod: { stackArn: "arn:stack:prod" } },
});
Expand All @@ -443,7 +462,7 @@ describe("CdkBackend.deploy", () => {
test("says to add a resource when there is no stack to remove either", async () => {
const input = await project();
await writeAssembly(input, [TARGET.name], { resources: METADATA_ONLY });
const subject = harness({ stackExists: false });
const subject = harness({ describedStack: null });

await expect(
collectDeploy(
Expand All @@ -460,7 +479,7 @@ describe("CdkBackend.deploy", () => {

await collectDeploy(subject.backend.deploy(input, deployInput()));

expect(subject.stackProbes).toEqual([]);
expect(subject.stackReads).toEqual([]);
});

test.each([
Expand Down Expand Up @@ -563,3 +582,117 @@ describe("CdkBackend.deploy", () => {
expect(subject.runs.map(({ operation }) => operation.kind)).toEqual(["bootstrap"]);
});
});

describe("CdkBackend.resolveDeployedResources", () => {
test("describes the stack once and returns only resources with deployed ID outputs", async () => {
const input = await project();
input.spec = ProjectSpecSchema.parse({
...input.spec,
runtimes: [
{
name: "checkout_agent",
build: "CodeZip",
entrypoint: "main.py",
codeLocation: "app/checkout_agent",
runtimeVersion: "PYTHON_3_14",
},
{
name: "inventory",
build: "CodeZip",
entrypoint: "main.py",
codeLocation: "app/inventory",
runtimeVersion: "PYTHON_3_14",
},
],
harnesses: [{ name: "support_agent", path: "app/support_agent" }],
});
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({
describedStack: {
StackName: "AgentCore-example-default",
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
Outputs: [
{
ExportName: "AgentCore-example-default-checkout-agent-RuntimeId",
OutputValue: "checkout_agent-AbCdEf1234",
},
{
ExportName: "AgentCore-example-default-Harness-support-agent-Id",
OutputValue: "support_agent-AbCdEf1234",
},
],
},
});

const resources = await subject.backend.resolveDeployedResources(input, { target: TARGET });

expect(resources).toEqual([
{ resourceType: "runtime", name: "checkout_agent", id: "checkout_agent-AbCdEf1234" },
{ resourceType: "harness", name: "support_agent", id: "support_agent-AbCdEf1234" },
]);
expect(subject.stackReads).toHaveLength(1);
});

test("fails without reading AWS when the target has no deployed stack ARN", async () => {
const input = await project();
const subject = harness({ describedStack: null });

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
).rejects.toThrow(/not deployed.*project deploy --target default/s);
expect(subject.stackReads).toEqual([]);
expect(subject.accountCredentials).toEqual([]);
});

test("fails actionably when the recorded stack no longer exists", async () => {
const input = await project();
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({ describedStack: null });

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
).rejects.toThrow(/not deployed.*project deploy --target default/s);
expect(subject.stackReads[0]?.stackName).toBe(STACK_ARN);
});

test("omits configured resources that have no deployed ID output", async () => {
const input = await project();
input.spec = ProjectSpecSchema.parse({
...input.spec,
runtimes: [
{
name: "checkout",
build: "CodeZip",
entrypoint: "main.py",
codeLocation: "app/checkout",
runtimeVersion: "PYTHON_3_14",
},
],
});
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({
describedStack: {
StackName: "AgentCore-example-default",
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
Outputs: [],
},
});

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
).resolves.toEqual([]);
});

test("rejects the wrong account before reading CloudFormation", async () => {
const input = await project();
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({ account: "999900001111" });

await expect(
subject.backend.resolveDeployedResources(input, { target: TARGET }),
).rejects.toThrow(/expects AWS account 111122223333.*999900001111/s);
expect(subject.stackReads).toEqual([]);
});
});
Loading
Loading