Skip to content
Closed
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
18 changes: 0 additions & 18 deletions src/core/project/templates/export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
MEMORY_NAME_NOT_FOUND_NOTE_CATEGORY,
MODEL_API_KEY_NOTE_CATEGORY,
buildExportNotesMarkdown,
formatExportNotes,
mapHarnessToExportPlan,
matchesAllowedTools,
type HarnessExportInput,
Expand Down Expand Up @@ -795,21 +794,4 @@ describe("export notes rendering", () => {
const markdown = buildExportNotesMarkdown([], "assistant", "assistantAgent", "v");
expect(markdown).toContain("No manual steps required.");
});

test("formatExportNotes renders a warning block or a quiet confirmation", () => {
expect(formatExportNotes([], "notes.md")).toEqual([
{ text: "No manual follow-up required. (Details: notes.md)", tone: "dim" },
]);
const lines = formatExportNotes(
[{ category: "Cat", message: "line one\nline two" }],
"notes.md",
);
expect(lines.map((line) => line.text)).toEqual([
"1 export note requiring manual follow-up:",
" - Cat",
" line one",
" line two",
"These notes are also saved to notes.md",
]);
});
});
31 changes: 1 addition & 30 deletions src/core/project/templates/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ export interface ExportNote {
message: string;
}

/** A single rendered output line + a tone the caller maps to its own styling. */
export interface ExportNoteLine {
text: string;
tone: "warn" | "dim";
}

/** Everything the mapper needs; all reads are done by the caller. */
export interface HarnessExportInput {
harnessName: string;
Expand Down Expand Up @@ -977,7 +971,7 @@ function fnmatch(pattern: string, str: string): boolean {
}

// ============================================================================
// EXPORT_NOTES.md + display formatting
// EXPORT_NOTES.md
// ============================================================================

/** Render the EXPORT_NOTES.md content written into the exported agent's directory. */
Expand Down Expand Up @@ -1010,26 +1004,3 @@ export function buildExportNotesMarkdown(
lines.push("");
return lines.join("\n");
}

/**
* Format export notes into styled lines for the export success path. Pure so the
* CLI (and a future TUI screen) render identical wording.
*/
export function formatExportNotes(notes: ExportNote[], notesFileHint: string): ExportNoteLine[] {
if (notes.length === 0) {
return [{ text: `No manual follow-up required. (Details: ${notesFileHint})`, tone: "dim" }];
}

const label = notes.length === 1 ? "note" : "notes";
const lines: ExportNoteLine[] = [
{ text: `${notes.length} export ${label} requiring manual follow-up:`, tone: "warn" },
];
for (const note of notes) {
lines.push({ text: ` - ${note.category}`, tone: "warn" });
for (const messageLine of note.message.split("\n")) {
lines.push({ text: ` ${messageLine}`, tone: "dim" });
}
}
lines.push({ text: `These notes are also saved to ${notesFileHint}`, tone: "dim" });
return lines;
}
10 changes: 9 additions & 1 deletion src/handlers/project/export/harness.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ describe("project export harness handler", () => {
});

expect(subject.io.stderr()).toContain(
"Exported harness 'exportme' to runtime agent 'exportmeAgent'",
`Next steps:\n Review the generated code in ${join("app", "exportmeAgent")}\n agentcore build\n agentcore deploy`,
);
expect(subject.io.stderr()).toContain(
`Exported harness 'exportme' to runtime agent 'exportmeAgent' (${join("app", "exportmeAgent")})\nNext steps:`,
);
expect(subject.io.stdout()).toBe("");
});
Expand Down Expand Up @@ -226,6 +229,8 @@ describe("project export harness handler", () => {
notesPath: join(projectRoot, "app", "exportmeAgent", "EXPORT_NOTES.md"),
notes: [],
});
expect(subject.io.stderr()).not.toContain("Next steps:");
expect(subject.io.stderr()).not.toContain("Exported harness");
});

test("exports a service harness by ARN, fetching from the ARN's region", async () => {
Expand Down Expand Up @@ -262,6 +267,9 @@ describe("project export harness handler", () => {
expect(spec.runtimes.map((runtime: { name: string }) => runtime.name)).toContain(
"exported_arn",
);
expect(subject.io.stderr()).toContain(
`Exported harness 'remote_harness' to runtime agent 'exported_arn' (${join("app", "exported_arn")})`,
);
});

test("defaults the --arn target name from the fetched harness name", async () => {
Expand Down
95 changes: 50 additions & 45 deletions src/handlers/project/export/harness.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import z from "zod";
import { relative } from "node:path";
import { InputValidationError, ResourceNotFoundError } from "../../../errors";
import { createHandler, flag, ProjectKey } from "../../../router";
import { JsonRendererKey } from "../../../tui";
import { runWithProgress } from "../../../tui/progress";
import { JsonKey } from "../../keys";
import { AgentNameSchema } from "../../../projectSchemas/runtime";
import { formatExportNotes } from "../../../core/project/templates/export";
import { assertMutuallyExclusiveFlags, coreOptsFromCtx } from "../../utils";
import type { ExportHarnessInput } from "../types";
import type { ExportProjectResourceConfig } from "./types";
Expand Down Expand Up @@ -36,50 +37,41 @@ export const createExportHarnessHandler = (config: ExportProjectResourceConfig)
const project = ctx.require(ProjectKey);
const jsonOutput = ctx.require(JsonKey);

let input: ExportHarnessInput;
if (flags.arn) {
config.io.stderr.write(`Fetching harness from the service\n`);
const harnessId = harnessIdFromArn(flags.arn);
// The ARN names the region the harness lives in and takes precedence over
// the CLI's resolved region, so service fetches never drift to ambient config.
const coreOpts = coreOptsFromCtx(ctx);
const region = regionFromHarnessArn(flags.arn);
const response = await config.core.harness.getHarness(harnessId, { ...coreOpts, region });
if (!response.harness) {
throw new ResourceNotFoundError(`no harness exists for '${flags.arn}'`);
}
const { spec, systemPrompt, notes, modelAdditionalParams } = mapServiceHarnessToSpec(
response.harness,
);
input = {
prefetched: { spec, systemPrompt, notes, modelAdditionalParams },
targetAgentName: resolveTargetAgentName(flags["target-agent-name"], spec.name),
};
} else {
input = {
harnessName: flags.name!,
targetAgentName: resolveTargetAgentName(flags["target-agent-name"], flags.name!),
};
}

// Progress goes to stderr, keeping stdout for machine output. Driven by
// hand because the result is the generator's return value.
const exportRun = config.projectManager.exportHarness(project, input);
let next = await exportRun.next();
while (!next.done) {
if (next.value.type === "step") config.io.stderr.write(`${next.value.message}\n`);
next = await exportRun.next();
}
const result = next.value;

config.io.stderr.write(
`Exported harness '${result.harnessName}' to runtime agent '${result.agentName}' (${result.agentPath})\n`,
);
for (const line of formatExportNotes(result.notes, result.notesPath)) {
config.io.stderr.write(`${line.text}\n`);
}
config.io.stderr.write(
"Next steps: review the generated code, then `agentcore build` and `agentcore deploy`\n",
const result = await runWithProgress(
(async function* () {
let input: ExportHarnessInput;
if (flags.arn) {
const harnessId = harnessIdFromArn(flags.arn);
// The ARN's region takes precedence over the CLI's resolved region.
const coreOpts = coreOptsFromCtx(ctx);
const region = regionFromHarnessArn(flags.arn);
yield { type: "step" as const, message: "Fetching harness from the service" };
const response = await config.core.harness.getHarness(harnessId, {
...coreOpts,
region,
});
if (!response.harness) {
throw new ResourceNotFoundError(`no harness exists for '${flags.arn}'`);
}
const { spec, systemPrompt, notes, modelAdditionalParams } = mapServiceHarnessToSpec(
response.harness,
);
input = {
prefetched: { spec, systemPrompt, notes, modelAdditionalParams },
targetAgentName: resolveTargetAgentName(flags["target-agent-name"], spec.name),
};
} else {
input = {
harnessName: flags.name!,
targetAgentName: resolveTargetAgentName(flags["target-agent-name"], flags.name!),
};
}
return yield* config.projectManager.exportHarness(project, input);
})(),
{
io: config.io,
interactive: jsonOutput ? false : undefined,
},
);

if (jsonOutput) {
Expand All @@ -90,7 +82,20 @@ export const createExportHarnessHandler = (config: ExportProjectResourceConfig)
notesPath: result.notesPath,
notes: result.notes,
});
return;
}

const agentPath = relative(process.cwd(), result.agentPath);
config.io.stderr.write(
`Exported harness '${result.harnessName}' to runtime agent '${result.agentName}' (${agentPath})\n`,
);
const reviewNotes =
result.notes.length > 0
? ` (${result.notes.length} manual follow-up${result.notes.length === 1 ? "" : "s"} in EXPORT_NOTES.md)`
: "";
config.io.stderr.write(
`Next steps:\n Review the generated code in ${agentPath}${reviewNotes}\n agentcore build\n agentcore deploy\n`,
);
},
});

Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import { CommandRunMetricEventKey, ValueContext } from "./router";
import { NpmCliVersionManager, printUpdateNotice } from "./cliVersionManager";
import { CliVersionManagerKey } from "./handlers/keys";

// Suppress only the AWS SDK's Node.js version support warning before constructing clients.
process.env.AWS_SDK_JS_NODE_VERSION_SUPPORT_WARNING_DISABLED = "true";

process.exit(
await runWithExitCode(async (argv: string[]) => {
const startTime = Date.now();
Expand Down
Loading