Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,6 +104,17 @@ agentcore # interactive TUI
│ ├── get # get an evaluator by id (type-agnostic)
│ ├── list # list evaluators (server-side paginated)
│ └── delete # delete an evaluator by id
├── project # manage an AgentCore project
│ ├── create # create a project
│ ├── add # add project resources
│ ├── remove # remove project resources
│ ├── dev # run the project locally
│ ├── deploy # deploy the project
│ ├── 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 deployable artifacts
└── config # read/write global config values
```

Expand All@@ -116,6 +127,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
Expand Up@@ -107,6 +107,7 @@ import { GatewayRuleScreen } from "../handlers/gateway/rule/screen.tsx";
import { GatewayRuleListScreen } from "../handlers/gateway/rule/list/screen.tsx";
import { GatewayRuleGetScreen } from "../handlers/gateway/rule/get/screen.tsx";
import { GatewayInvokeScreen } from "../handlers/gateway/invoke/screen.tsx";
import { ProjectInvokePickerScreen } from "../handlers/project/invoke/screen.tsx";
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
import type { Context } from "../router";

Expand DownExpand Up@@ -140,6 +141,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
133 changes: 132 additions & 1 deletion src/core/project/backends/cdk.test.ts
Original file line numberDiff line numberDiff 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 DownExpand Up@@ -122,6 +127,7 @@ type HarnessOptions = {
bootstrapError?: Error;
/** Whether CloudFormation still holds the target's stack. Defaults to present. */
stackExists?: boolean;
stack?: Stack;
};

function harness(options: HarnessOptions = {}) {
Expand All@@ -133,6 +139,8 @@ function harness(options: HarnessOptions = {}) {
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 DownExpand Up@@ -194,6 +202,10 @@ function harness(options: HarnessOptions = {}) {
},
};
},
describeStack: async (region, provider, stackName) => {
stackReads.push({ stackName, region, credentials: provider });
return options.stack;
},
});

return {
Expand All@@ -207,6 +219,7 @@ function harness(options: HarnessOptions = {}) {
credentials,
runs,
stackProbes,
stackReads,
templateLoads: () => templateLoads,
templateCleanups: () => templateCleanups,
};
Expand DownExpand Up@@ -563,3 +576,121 @@ describe("CdkBackend.deploy", () => {
expect(subject.runs.map(({ operation }) => operation.kind)).toEqual(["bootstrap"]);
});
});

describe("CdkBackend.resolveDeployedResource", () => {
test.each([
{
resourceType: "runtime" as const,
name: "checkout_agent",
exportName: "AgentCore-example-default-checkout-agent-RuntimeId",
id: "checkout_agent-AbCdEf1234",
},
{
resourceType: "harness" as const,
name: "support_agent",
exportName: "AgentCore-example-default-Harness-support-agent-Id",
id: "support_agent-AbCdEf1234",
},
])(
"reads deployed state and resolves a $resourceType ID from its live stack",
async (example) => {
const input = await project();
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({
stack: {
StackName: "AgentCore-example-default",
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
Outputs: [
{
ExportName: example.exportName,
OutputValue: example.id,
},
],
},
});

const id = await subject.backend.resolveDeployedResource(input, {
target: TARGET,
resourceType: example.resourceType,
name: example.name,
});

expect(id).toBe(example.id);
expect(subject.stackReads).toEqual([
{
stackName: STACK_ARN,
region: TARGET.region,
credentials: subject.credentials,
},
]);
expect(subject.accountCredentials).toEqual([subject.credentials]);
},
);

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

await expect(
subject.backend.resolveDeployedResource(input, {
target: TARGET,
resourceType: "harness",
name: "support",
}),
).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();

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

test("fails when the live stack has no output for the selected resource", async () => {
const input = await project();
await updateTargetState(json, input.rootPath, TARGET.name, { stackArn: STACK_ARN });
const subject = harness({
stack: {
StackName: "AgentCore-example-default",
CreationTime: new Date(0),
StackStatus: "CREATE_COMPLETE",
Outputs: [],
},
});

await expect(
subject.backend.resolveDeployedResource(input, {
target: TARGET,
resourceType: "runtime",
name: "checkout",
}),
).rejects.toThrow(/Runtime 'checkout'.*not deployed.*default/s);
});

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.resolveDeployedResource(input, {
target: TARGET,
resourceType: "runtime",
name: "checkout",
}),
).rejects.toThrow(/expects AWS account 111122223333.*999900001111/s);
expect(subject.stackReads).toEqual([]);
});
});
Loading
Loading