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
85 changes: 49 additions & 36 deletions src/core/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { CoreClient } from "./index";
import type { ClientConfig } from "./types";
import { toClientConfig } from "./utils";
import { createSilentLogger } from "../testing";

// A minimal stand-in for the SDK clients; CoreClient only stores and returns
// them, so an opaque tagged object is enough to assert identity/caching.
Expand All @@ -24,14 +25,15 @@ function fakeIam(config: ClientConfig): IAMClient {

test("control() constructs a client once per config and caches it", () => {
let built = 0;
const core = new CoreClient(
(config) => {
const core = new CoreClient({
createControlClient: (config) => {
built++;
return fakeControl(config);
},
fakeData,
fakeIam,
);
createDataClient: fakeData,
createIamClient: fakeIam,
logger: createSilentLogger(),
});

const a = core.control({ region: "us-east-1" });
const b = core.control({ region: "us-east-1" });
Expand All @@ -42,14 +44,15 @@ test("control() constructs a client once per config and caches it", () => {

test("control() builds a distinct client per distinct config", () => {
let built = 0;
const core = new CoreClient(
(config) => {
const core = new CoreClient({
createControlClient: (config) => {
built++;
return fakeControl(config);
},
fakeData,
fakeIam,
);
createDataClient: fakeData,
createIamClient: fakeIam,
logger: createSilentLogger(),
});

core.control({ region: "us-east-1" });
core.control({ region: "us-west-2" });
Expand All @@ -61,17 +64,18 @@ test("control() builds a distinct client per distinct config", () => {
test("data() caches independently of control()", () => {
let controlBuilt = 0;
let dataBuilt = 0;
const core = new CoreClient(
(config) => {
const core = new CoreClient({
createControlClient: (config) => {
controlBuilt++;
return fakeControl(config);
},
(config) => {
createDataClient: (config) => {
dataBuilt++;
return fakeData(config);
},
fakeIam,
);
createIamClient: fakeIam,
logger: createSilentLogger(),
});

core.control({ region: "us-east-1" });
const d1 = core.data({ region: "us-east-1" });
Expand All @@ -83,7 +87,12 @@ test("data() caches independently of control()", () => {
});

test("exposes a harness sub-client", () => {
const core = new CoreClient(fakeControl, fakeData, fakeIam);
const core = new CoreClient({
createControlClient: fakeControl,
createDataClient: fakeData,
createIamClient: fakeIam,
logger: createSilentLogger(),
});
expect(core.harness).toBeDefined();
});

Expand All @@ -93,9 +102,9 @@ test("invokeHarness sends an InvokeHarnessCommand on the data client with the ab
const sent: { command: unknown; options: unknown }[] = [];
const configs: ClientConfig[] = [];
const response = { stream: undefined };
const core = new CoreClient(
fakeControl,
(config) => {
const core = new CoreClient({
createControlClient: fakeControl,
createDataClient: (config) => {
configs.push(config);
return {
config,
Expand All @@ -106,8 +115,9 @@ test("invokeHarness sends an InvokeHarnessCommand on the data client with the ab
},
} as unknown as BedrockAgentCoreClient;
},
fakeIam,
);
createIamClient: fakeIam,
logger: createSilentLogger(),
});

const request = {
harnessArn: "arn:aws:bedrock-agentcore:us-east-1:123:harness/h-1",
Expand Down Expand Up @@ -138,16 +148,17 @@ test("invokeHarness stream iteration rejects promptly when aborted mid-stream",
await new Promise(() => {});
},
};
const core = new CoreClient(
fakeControl,
(config) =>
const core = new CoreClient({
createControlClient: fakeControl,
createDataClient: (config) =>
({
config,
kind: "data",
send: async () => ({ stream: hangingStream }),
}) as unknown as BedrockAgentCoreClient,
fakeIam,
);
createIamClient: fakeIam,
logger: createSilentLogger(),
});

const controller = new AbortController();
const response = await core.harness.invokeHarness(
Expand All @@ -166,9 +177,9 @@ test("invokeHarness stream iteration rejects promptly when aborted mid-stream",

test("invokeAgentRuntimeCommand sends the command on the data client with the abort signal", async () => {
const sent: { command: unknown; options: unknown }[] = [];
const core = new CoreClient(
fakeControl,
(config) =>
const core = new CoreClient({
createControlClient: fakeControl,
createDataClient: (config) =>
({
config,
kind: "data",
Expand All @@ -177,8 +188,9 @@ test("invokeAgentRuntimeCommand sends the command on the data client with the ab
return { statusCode: 200, stream: undefined };
},
}) as unknown as BedrockAgentCoreClient,
fakeIam,
);
createIamClient: fakeIam,
logger: createSilentLogger(),
});

const request = {
agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123:harness/h-1",
Expand All @@ -195,16 +207,17 @@ test("invokeAgentRuntimeCommand sends the command on the data client with the ab

test("invokeHarness returns the stream untouched when no abort signal is given", async () => {
const stream = (async function* () {})();
const core = new CoreClient(
fakeControl,
(config) =>
const core = new CoreClient({
createControlClient: fakeControl,
createDataClient: (config) =>
({
config,
kind: "data",
send: async () => ({ stream }),
}) as unknown as BedrockAgentCoreClient,
fakeIam,
);
createIamClient: fakeIam,
logger: createSilentLogger(),
});

const response = await core.harness.invokeHarness(
{ harnessArn: "arn", runtimeSessionId: "s".repeat(40), messages: [] },
Expand Down
32 changes: 27 additions & 5 deletions src/core/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type {
CreateDataClient,
CreateIamClient,
} from "./types";
import type { Logger } from "../logging";
import type { ProjectManager } from "../handlers/project/types";
import { FsProjectManager } from "./project";

export type {
AwsClients,
Expand All @@ -19,6 +22,13 @@ export type {
CreateIamClient,
} from "./types";

type CoreClientConfig = {
createControlClient: CreateControlClient;
createDataClient: CreateDataClient;
createIamClient: CreateIamClient;
logger: Logger;
};

// CoreClient is the single entry point to the Bedrock AgentCore APIs. It owns the
// underlying SDK clients (one per config, created on demand from the injected
// factories) and exposes feature-scoped sub-clients such as `harness`, keeping the
Expand All @@ -28,15 +38,27 @@ export class CoreClient implements AwsClients {
private dataClients = new Map<string, BedrockAgentCoreClient>();
private iamClients = new Map<string, IAMClient>();

private readonly createControlClient: CreateControlClient;
private readonly createDataClient: CreateDataClient;
private readonly createIamClient: CreateIamClient;
private logger: Logger;

// Feature-scoped sub-clients. Access as e.g. `coreClient.harness.getHarness(...)`.
readonly harness: HarnessClient = new HarnessClient(this);
readonly runtime: RuntimeClient = new RuntimeClient(this);

constructor(
private readonly createControlClient: CreateControlClient,
private readonly createDataClient: CreateDataClient,
private readonly createIamClient: CreateIamClient,
) {}
readonly projectManager: ProjectManager;

constructor(config: CoreClientConfig) {
this.createControlClient = config.createControlClient;
this.createDataClient = config.createDataClient;
this.createIamClient = config.createIamClient;
this.logger = config.logger;

this.projectManager = new FsProjectManager({
logger: this.logger.child({ module: "projectManager" }),
});
}

// control returns the control-plane client for `config`, creating and caching it
// on first use.
Expand Down
1 change: 1 addition & 0 deletions src/core/project/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FsProjectManager } from "./manager";
26 changes: 26 additions & 0 deletions src/core/project/manager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {
CreateProjectInput,
ResolveProjectInput,
Project,
ProjectManager,
} from "../../handlers/project/types";
import type { Logger } from "../../logging";

type ProjectManagerConfig = {
logger: Logger;
};

/**
* An implementation of {@link ProjectManager} that relies on the local file system to manage projects.
*/
export class FsProjectManager implements ProjectManager {
constructor(_config: ProjectManagerConfig) {}

public resolve(_input: ResolveProjectInput): Promise<Project> {
throw new Error(`ProjectManager.resolve is not implemented yet`);
}

public create(_input: CreateProjectInput): Promise<Project> {
throw new Error(`ProjectManager.create is not implemented yet`);
}
}
7 changes: 6 additions & 1 deletion src/handlers/harness/harness.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ const REGION = "us-west-2";
// and returns whatever the command wrote to stdout.
async function run(args: string[]): Promise<string> {
const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES);
const core = new CoreClient(createControlClient, createDataClient, createIamClient);
const core = new CoreClient({
createControlClient,
createDataClient,
createIamClient,
logger: createSilentLogger(),
});
const io = testIO();
const root = createRootHandler(core, { io: io.io, logger: createSilentLogger() });
await root.route(["node", "agentcore", ...args, "--region", REGION]);
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router
root.handler(createHarnessHandler(core, io));
root.handler(createRuntimeHandler(core, io));
root.handler(createConfigHandler(io));
root.handler(createProjectHandler());
root.handler(createProjectHandler({ projectManager: core.projectManager }));

// Invoking with no subcommand launches the interactive TUI.
root.default(renderTui(core, io));
Expand Down
15 changes: 10 additions & 5 deletions src/handlers/project/create/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import z from "zod";
import { createHandler, flag } from "../../../router";
import { PROJECT_TEMPLATES, type ProjectManager } from "../types";

export const PROJECT_TEMPLATES = ["placeholder"] as const;
type CreateProjectHandlerConfig = {
projectManager: ProjectManager;
};

export const createCreateProjectHandler = () =>
export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) =>
createHandler({
name: "create",
description: "create a new AgentCore project",
flags: [
flag(
"template",
"project template to scaffold from",
z.enum(PROJECT_TEMPLATES).default("placeholder"),
z.enum(PROJECT_TEMPLATES).default(PROJECT_TEMPLATES.BAREBONES),
),
],
handle: async () => {
throw new Error("`agentcore project create` is not implemented yet");
handle: async (_ctx, flags) => {
await config.projectManager.create({
template: flags.template,
});
},
});
9 changes: 7 additions & 2 deletions src/handlers/project/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Router } from "../../router";
import { createCreateProjectHandler } from "./create";
import type { ProjectManager } from "./types";

export function createProjectHandler(): Router {
type ProjectHandlerConfig = {
projectManager: ProjectManager;
};

export function createProjectHandler(config: ProjectHandlerConfig): Router {
const project = new Router("project", "manage an AgentCore project");

project.handler(createCreateProjectHandler());
project.handler(createCreateProjectHandler({ projectManager: config.projectManager }));

return project;
}
2 changes: 1 addition & 1 deletion src/handlers/project/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("project create", () => {
});

test("accepts a known --template value", async () => {
await expect(run(["create", "--template", "placeholder"])).rejects.toThrow(/not implemented/);
await expect(run(["create", "--template", "barebones"])).rejects.toThrow(/not implemented/);
});

test("rejects an unknown --template value", async () => {
Expand Down
31 changes: 31 additions & 0 deletions src/handlers/project/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/** Available project templates for scaffolding new AgentCore projects. */
export const PROJECT_TEMPLATES = {
BAREBONES: "barebones",
} as const;

export type ProjectTemplate = (typeof PROJECT_TEMPLATES)[keyof typeof PROJECT_TEMPLATES];

export type CreateProjectInput = {
/** The project template to scaffold from. */
template: ProjectTemplate;
};

export type ResolveProjectInput = {
/** A path to search from when locating the project root. */
filePath: string;
};

export type Project = {
name: string;
};

/**
* The primary interface for interacting with projects
*/
export interface ProjectManager {
/** Scaffold a new AgentCore project from the given template. */
create(input: CreateProjectInput): Promise<Project>;

/** Locate an existing AgentCore project. Returns undefined if no project can be found. */
resolve(input: ResolveProjectInput): Promise<Project | undefined>;
}
7 changes: 6 additions & 1 deletion src/handlers/runtime/runtime.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ const MISSING_RUNTIME_ID = "missing_runtime-0000000000";

function createFixtureCore(): CoreClient {
const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES);
return new CoreClient(createControlClient, createDataClient, createIamClient);
return new CoreClient({
createControlClient,
createDataClient,
createIamClient,
logger: createSilentLogger(),
});
}

async function run(args: string[]): Promise<string> {
Expand Down
Loading
Loading