diff --git a/src/cli/aws/__tests__/agentcore-invoke-qualifier.test.ts b/src/cli/aws/__tests__/agentcore-invoke-qualifier.test.ts new file mode 100644 index 000000000..f1f7ca5d8 --- /dev/null +++ b/src/cli/aws/__tests__/agentcore-invoke-qualifier.test.ts @@ -0,0 +1,117 @@ +import { invokeAgentRuntime, invokeAgentRuntimeStreaming, invokeAguiRuntime } from '../agentcore.js'; +import type { InvokeAgentRuntimeOptions } from '../agentcore.js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const commandArgs: Record[] = []; +const mockSend = vi.fn(); + +vi.mock('@aws-sdk/client-bedrock-agentcore', () => { + class MockBedrockAgentCoreClient { + send = mockSend; + middlewareStack = { add: vi.fn() }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor(_config: unknown) {} + } + class MockInvokeAgentRuntimeCommand { + input: Record; + constructor(args: Record) { + this.input = args; + commandArgs.push(args); + } + } + return { + BedrockAgentCoreClient: MockBedrockAgentCoreClient, + InvokeAgentRuntimeCommand: MockInvokeAgentRuntimeCommand, + }; +}); + +vi.mock('../account.js', () => ({ + getCredentialProvider: vi + .fn() + .mockReturnValue(() => Promise.resolve({ accessKeyId: 'test', secretAccessKey: 'test' })), +})); + +function makeByteResponse(body: string) { + return { + runtimeSessionId: 'sess-1', + response: { + transformToByteArray: () => Promise.resolve(new TextEncoder().encode(body)), + }, + }; +} + +function makeStreamResponse(body: string) { + return { + runtimeSessionId: 'sess-1', + response: { + transformToWebStream: () => + new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(body)); + controller.close(); + }, + }), + }, + }; +} + +const BASE: InvokeAgentRuntimeOptions = { + region: 'us-east-1', + runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123:runtime/r', + payload: 'hi', +}; + +describe('SigV4 invoke qualifier', () => { + beforeEach(() => { + commandArgs.length = 0; + mockSend.mockReset(); + }); + + it('sets qualifier on the non-streaming command when endpoint is provided', async () => { + mockSend.mockResolvedValue(makeByteResponse('{"result":"ok"}')); + await invokeAgentRuntime({ ...BASE, endpoint: 'prod' }); + expect(commandArgs[0]?.qualifier).toBe('prod'); + }); + + it('omits qualifier on the non-streaming command when no endpoint is provided', async () => { + mockSend.mockResolvedValue(makeByteResponse('{"result":"ok"}')); + await invokeAgentRuntime({ ...BASE }); + expect(commandArgs[0]).not.toHaveProperty('qualifier'); + }); + + it('sets qualifier on the streaming command when endpoint is provided', async () => { + mockSend.mockResolvedValue(makeStreamResponse('data: "hi"\n')); + const { stream } = await invokeAgentRuntimeStreaming({ ...BASE, endpoint: 'staging' }); + for await (const _ of stream) { + // drain + } + expect(commandArgs[0]?.qualifier).toBe('staging'); + }); + + it('omits qualifier on the streaming command when no endpoint is provided', async () => { + mockSend.mockResolvedValue(makeStreamResponse('data: "hi"\n')); + const { stream } = await invokeAgentRuntimeStreaming({ ...BASE }); + for await (const _ of stream) { + // drain + } + expect(commandArgs[0]).not.toHaveProperty('qualifier'); + }); + + it('sets qualifier on the AGUI command when endpoint is provided', async () => { + mockSend.mockResolvedValue(makeStreamResponse('data: {}\n\n')); + await invokeAguiRuntime( + { region: BASE.region, runtimeArn: BASE.runtimeArn, endpoint: 'prod' }, + { threadId: 't', runId: 'r', messages: [], tools: [], context: [], state: {}, forwardedProps: {} } + ); + expect(commandArgs[0]?.qualifier).toBe('prod'); + }); + + it('omits qualifier on the AGUI command when no endpoint is provided', async () => { + mockSend.mockResolvedValue(makeStreamResponse('data: {}\n\n')); + await invokeAguiRuntime( + { region: BASE.region, runtimeArn: BASE.runtimeArn }, + { threadId: 't', runId: 'r', messages: [], tools: [], context: [], state: {}, forwardedProps: {} } + ); + expect(commandArgs[0]).not.toHaveProperty('qualifier'); + }); +}); diff --git a/src/cli/aws/agentcore.ts b/src/cli/aws/agentcore.ts index b91df356d..fde71540a 100644 --- a/src/cli/aws/agentcore.ts +++ b/src/cli/aws/agentcore.ts @@ -367,6 +367,7 @@ export async function invokeAgentRuntimeStreaming(options: InvokeAgentRuntimeOpt runtimeSessionId: options.sessionId, runtimeUserId: options.userId ?? DEFAULT_RUNTIME_USER_ID, ...(options.baggage && { baggage: options.baggage }), + ...(options.endpoint && { qualifier: options.endpoint }), }); const response = await client.send(command); @@ -463,6 +464,7 @@ export async function invokeAgentRuntime(options: InvokeAgentRuntimeOptions): Pr runtimeSessionId: options.sessionId, runtimeUserId: options.userId ?? DEFAULT_RUNTIME_USER_ID, ...(options.baggage && { baggage: options.baggage }), + ...(options.endpoint && { qualifier: options.endpoint }), }); const response = await client.send(command); @@ -1059,6 +1061,8 @@ export interface AguiInvokeOptions { headers?: Record; /** Bearer token for CUSTOM_JWT auth — not yet supported for AGUI, will throw if provided */ bearerToken?: string; + /** Runtime endpoint qualifier (the endpoint NAME, e.g. prod/staging). Defaults to DEFAULT when omitted. */ + endpoint?: string; } export interface AguiStreamingInvokeResult { @@ -1090,6 +1094,7 @@ export async function invokeAguiRuntime( accept: 'text/event-stream', runtimeSessionId: options.sessionId, runtimeUserId: options.userId ?? DEFAULT_RUNTIME_USER_ID, + ...(options.endpoint && { qualifier: options.endpoint }), }); const response = await client.send(command); diff --git a/src/cli/aws/index.ts b/src/cli/aws/index.ts index 09851e678..6cdbbf1df 100644 --- a/src/cli/aws/index.ts +++ b/src/cli/aws/index.ts @@ -16,7 +16,14 @@ export { type AgentRuntimeStatusResult, type GetAgentRuntimeStatusOptions, } from './agentcore-control'; -export { streamLogs, searchLogs, type LogEvent, type StreamLogsOptions, type SearchLogsOptions } from './cloudwatch'; +export { + streamLogs, + searchLogs, + resolveEndpointName, + type LogEvent, + type StreamLogsOptions, + type SearchLogsOptions, +} from './cloudwatch'; export { enableTransactionSearch } from './transaction-search'; export { startPolicyGeneration, diff --git a/src/cli/commands/invoke/__tests__/invoke.test.ts b/src/cli/commands/invoke/__tests__/invoke.test.ts index 49ac97335..721a4c936 100644 --- a/src/cli/commands/invoke/__tests__/invoke.test.ts +++ b/src/cli/commands/invoke/__tests__/invoke.test.ts @@ -270,4 +270,52 @@ describe('invoke command', () => { expect(result.stderr).not.toContain('requires an interactive terminal'); }); }); + + // -------------------------------------------------------------------------- + // Endpoint mode routing. + // + // The same no-TTY signature lets us prove the env-var fallback no longer + // silently drops into the TUI (which would invoke the DEFAULT endpoint). A + // non-DEFAULT resolved endpoint — whether from --endpoint or the + // AGENTCORE_RUNTIME_ENDPOINT env var — forces CLI mode. + // -------------------------------------------------------------------------- + describe('endpoint mode routing', () => { + it('AGENTCORE_RUNTIME_ENDPOINT (no prompt/flag/json) forces CLI mode instead of silently routing to the TUI', async () => { + // Regression for #986/#1554: previously the env var alone never forced CLI + // mode, so the user dropped into the TUI and silently hit DEFAULT. NO --json + // and NO other CLI flag here on purpose — the resolved non-DEFAULT endpoint + // (`endpoint !== undefined` in the gate) is the ONLY thing forcing CLI mode, + // so this exercises the new clause directly. CLI mode reaches the action + // layer, which resolves the invoke target and fails with "No deployed + // targets found" (this project has an agent but no deployed state). If the + // `endpoint !== undefined` clause were removed, this would instead route to + // the TUI and hit the requireTTY guard ("requires an interactive terminal"). + const result = await runCLI(['invoke'], projectDir, { + env: { ...telemetry.env, AGENTCORE_RUNTIME_ENDPOINT: 'staging' }, + }); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('No deployed targets found'); + expect(result.stderr).not.toContain('requires an interactive terminal'); + telemetry.assertMetricEmitted({ command: 'invoke', endpoint_source: 'env' }); + }); + + it('records endpoint_source=flag when --endpoint is passed', async () => { + // --endpoint forces CLI mode; the agent has no named endpoint configured so + // the action layer rejects it — but the telemetry attr is emitted regardless. + const result = await runCLI(['invoke', 'hi', '--endpoint', 'prod', '--json'], projectDir, { + env: telemetry.env, + }); + expect(result.exitCode).toBe(1); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(false); + expect(result.stderr).not.toContain('requires an interactive terminal'); + telemetry.assertMetricEmitted({ command: 'invoke', endpoint_source: 'flag' }); + }); + + it('records endpoint_source=default when neither --endpoint nor the env var is set', async () => { + const result = await runCLI(['invoke', 'hi', '--json'], projectDir, { env: telemetry.env }); + expect(result.exitCode).toBe(1); + telemetry.assertMetricEmitted({ command: 'invoke', endpoint_source: 'default' }); + }); + }); }); diff --git a/src/cli/commands/invoke/__tests__/resolve.test.ts b/src/cli/commands/invoke/__tests__/resolve.test.ts index 02c2f5359..e49a6aa0b 100644 --- a/src/cli/commands/invoke/__tests__/resolve.test.ts +++ b/src/cli/commands/invoke/__tests__/resolve.test.ts @@ -257,6 +257,96 @@ describe('resolveInvokeTarget', () => { expect(result.runtimeArn).toContain('rt-b'); }); + describe('named runtime endpoint resolution', () => { + it('resolves an endpoint configured on the runtime to a qualifier', async () => { + const project = makeProject({ + runtimes: [ + { + name: 'my-agent', + build: 'CodeZip', + codeLocation: './agents/my-agent', + entrypoint: 'main.py', + runtimeVersion: '1.0', + networkMode: 'PUBLIC', + endpoints: { prod: { version: 2 } }, + }, + ] as unknown as AgentCoreProjectSpec['runtimes'], + }); + + const result = await resolveInvokeTarget({ + project, + deployedState: makeDeployedState(), + awsTargets: makeAwsTargets(), + endpointName: 'prod', + }); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.endpoint).toBe('prod'); + }); + + it('resolves an endpoint present only in deployed runtimeEndpoints', async () => { + const deployedState = { + targets: { + default: { + resources: { + runtimes: { + 'my-agent': { + runtimeId: 'rt-123', + runtimeArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789:runtime/rt-123', + roleArn: 'arn:aws:iam::123456789:role/test-role', + }, + }, + runtimeEndpoints: { + 'my-agent/staging': { + endpointId: 'ep-1', + endpointArn: 'arn:aws:bedrock-agentcore:us-east-1:123:runtime-endpoint/ep-1', + }, + }, + }, + }, + }, + } as unknown as DeployedState; + + const result = await resolveInvokeTarget({ + project: makeProject(), + deployedState, + awsTargets: makeAwsTargets(), + endpointName: 'staging', + }); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.endpoint).toBe('staging'); + }); + + it('returns ResourceNotFoundError for an unknown endpoint name', async () => { + const result = await resolveInvokeTarget({ + project: makeProject(), + deployedState: makeDeployedState(), + awsTargets: makeAwsTargets(), + endpointName: 'nope', + }); + + expect(result.success).toBe(false); + if (result.success) return; + expect(result.error).toBeInstanceOf(ResourceNotFoundError); + expect(result.error.message).toContain("Endpoint 'nope' not found"); + }); + + it('leaves endpoint undefined when no endpointName is provided', async () => { + const result = await resolveInvokeTarget({ + project: makeProject(), + deployedState: makeDeployedState(), + awsTargets: makeAwsTargets(), + }); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.endpoint).toBeUndefined(); + }); + }); + describe('CUSTOM_JWT token resolution', () => { function makeJwtProject(): AgentCoreProjectSpec { return makeProject({ diff --git a/src/cli/commands/invoke/__tests__/utils.test.ts b/src/cli/commands/invoke/__tests__/utils.test.ts index b0f87bb9f..297cc7ebb 100644 --- a/src/cli/commands/invoke/__tests__/utils.test.ts +++ b/src/cli/commands/invoke/__tests__/utils.test.ts @@ -1,5 +1,35 @@ -import { computeInvokeAttrs } from '../utils'; -import { describe, expect, it } from 'vitest'; +import { computeEndpointSource, computeInvokeAttrs } from '../utils'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +describe('computeEndpointSource', () => { + const original = process.env.AGENTCORE_RUNTIME_ENDPOINT; + + beforeEach(() => { + delete process.env.AGENTCORE_RUNTIME_ENDPOINT; + }); + + afterEach(() => { + if (original === undefined) { + delete process.env.AGENTCORE_RUNTIME_ENDPOINT; + } else { + process.env.AGENTCORE_RUNTIME_ENDPOINT = original; + } + }); + + it("returns 'flag' when the --endpoint flag is set (even if the env var is also set)", () => { + process.env.AGENTCORE_RUNTIME_ENDPOINT = 'staging'; + expect(computeEndpointSource('prod')).toBe('flag'); + }); + + it("returns 'env' when only the env var is set", () => { + process.env.AGENTCORE_RUNTIME_ENDPOINT = 'staging'; + expect(computeEndpointSource(undefined)).toBe('env'); + }); + + it("returns 'default' when neither flag nor env var is set", () => { + expect(computeEndpointSource(undefined)).toBe('default'); + }); +}); describe('computeInvokeAttrs', () => { it('returns harness when harnessName is set', () => { @@ -77,4 +107,34 @@ describe('computeInvokeAttrs', () => { }); expect(attrs.agent_protocol).toBe('mcp'); }); + + it("defaults endpoint_source to 'default' when omitted", () => { + const attrs = computeInvokeAttrs({ + harnessCount: 0, + runtimeCount: 1, + stream: false, + hasSessionId: false, + }); + expect(attrs.endpoint_source).toBe('default'); + }); + + it('passes through the provided endpoint_source', () => { + const flag = computeInvokeAttrs({ + harnessCount: 0, + runtimeCount: 1, + stream: false, + hasSessionId: false, + endpointSource: 'flag', + }); + expect(flag.endpoint_source).toBe('flag'); + + const env = computeInvokeAttrs({ + harnessCount: 0, + runtimeCount: 1, + stream: false, + hasSessionId: false, + endpointSource: 'env', + }); + expect(env.endpoint_source).toBe('env'); + }); }); diff --git a/src/cli/commands/invoke/action.ts b/src/cli/commands/invoke/action.ts index 7840b50b0..8e0439274 100644 --- a/src/cli/commands/invoke/action.ts +++ b/src/cli/commands/invoke/action.ts @@ -289,6 +289,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption awsTargets, agentName: options.agentName, targetName: options.targetName, + endpointName: options.endpoint, bearerToken: options.bearerToken, sessionId: options.sessionId, }); @@ -297,7 +298,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption return { success: false, error: resolved.error }; } - const { agentSpec, targetName: selectedTargetName, targetConfig, runtimeArn, baggage } = resolved; + const { agentSpec, targetName: selectedTargetName, targetConfig, runtimeArn, endpoint, baggage } = resolved; options = { ...options, bearerToken: resolved.bearerToken ?? options.bearerToken, @@ -641,6 +642,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption logger, headers: options.headers, bearerToken: options.bearerToken, + endpoint, }, aguiInput ); @@ -714,6 +716,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption headers: options.headers, bearerToken: options.bearerToken, baggage, + endpoint, paymentInstrumentId: options.paymentInstrumentId, paymentSessionId: options.paymentSessionId, paymentUserId: options.paymentUserId, @@ -751,6 +754,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption headers: options.headers, bearerToken: options.bearerToken, baggage, + endpoint, paymentInstrumentId: options.paymentInstrumentId, paymentSessionId: options.paymentSessionId, paymentUserId: options.paymentUserId, diff --git a/src/cli/commands/invoke/command.tsx b/src/cli/commands/invoke/command.tsx index 574aa1357..b8171be31 100644 --- a/src/cli/commands/invoke/command.tsx +++ b/src/cli/commands/invoke/command.tsx @@ -1,5 +1,6 @@ import { ValidationError, serializeResult } from '../../../lib'; -import { COMMAND_DESCRIPTIONS } from '../../constants'; +import { resolveEndpointName } from '../../aws'; +import { COMMAND_DESCRIPTIONS, DEFAULT_ENDPOINT_NAME } from '../../constants'; import { getErrorMessage } from '../../errors'; import { ADDITIONAL_PARAMS_JSON_ERROR } from '../../primitives/constants'; import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js'; @@ -9,7 +10,7 @@ import { parseHeaderFlags } from '../shared/header-utils'; import { type InvokeContext, handleHarnessInvokeByArn, handleInvoke, loadInvokeConfig } from './action'; import { resolvePrompt } from './resolve-prompt'; import type { InvokeOptions, InvokeResult } from './types'; -import { computeInvokeAttrs } from './utils'; +import { computeEndpointSource, computeInvokeAttrs } from './utils'; import { validateInvokeOptions } from './validate'; import type { Command } from '@commander-js/extra-typings'; import { Text, render } from 'ink'; @@ -138,6 +139,10 @@ export const registerInvoke = (program: Command) => { 'Read the prompt from a file (for long or structured payloads that exceed shell arg limits) [non-interactive]' ) .option('--runtime ', 'Select specific runtime [non-interactive]') + .option( + '--endpoint ', + 'Target a named runtime endpoint (version alias, e.g. prod/staging). Defaults to AGENTCORE_RUNTIME_ENDPOINT env var, then DEFAULT [non-interactive]' + ) .option('--gateway ', 'Invoke through a gateway [non-interactive]') .option('--gateway-target-name ', 'HTTP runtime target on the gateway [non-interactive]') .option('--target ', 'Select deployment target [non-interactive]') @@ -292,6 +297,7 @@ Model & Runtime Overrides (harness only) [non-interactive] prompt?: string; promptFile?: string; runtime?: string; + endpoint?: string; gateway?: string; gatewayTargetName?: string; target?: string; @@ -355,6 +361,17 @@ Model & Runtime Overrides (harness only) [non-interactive] stdinPiped: !process.stdin.isTTY, }); + // Resolve the named endpoint once, before the CLI-mode gate, so the + // AGENTCORE_RUNTIME_ENDPOINT env-var fallback is honored in BOTH flows: + // --endpoint flag → env var → DEFAULT. A non-DEFAULT resolution forces + // CLI mode (so `AGENTCORE_RUNTIME_ENDPOINT=staging agentcore invoke` does + // not silently drop into the TUI and hit DEFAULT) and is also threaded + // into the TUI route below for interactive parity. Leave it undefined for + // DEFAULT so the SigV4 qualifier is omitted rather than sent explicitly. + const resolvedEndpoint = resolveEndpointName(cliOptions.endpoint); + const endpoint = resolvedEndpoint === DEFAULT_ENDPOINT_NAME ? undefined : resolvedEndpoint; + const endpointSource = computeEndpointSource(cliOptions.endpoint); + // CLI mode if any CLI-specific options provided, prompt resolved, or prompt resolution failed // (follows deploy command pattern) if ( @@ -365,6 +382,8 @@ Model & Runtime Overrides (harness only) [non-interactive] cliOptions.gatewayTargetName || cliOptions.stream || cliOptions.runtime || + cliOptions.endpoint || + endpoint !== undefined || cliOptions.gateway || cliOptions.tool || cliOptions.exec || @@ -389,6 +408,7 @@ Model & Runtime Overrides (harness only) [non-interactive] hasSessionId: !!cliOptions.sessionId, bearerToken: cliOptions.bearerToken, agentProtocol: agentProtocol ?? (cliOptions.tool ? 'mcp' : undefined), + endpointSource, }), async (): Promise => { if (!resolved.success) { @@ -418,6 +438,7 @@ Model & Runtime Overrides (harness only) [non-interactive] const options: InvokeOptions = { prompt: resolved.prompt, agentName: cliOptions.runtime, + endpoint, gateway: cliOptions.gateway, gatewayTarget: cliOptions.gatewayTargetName, targetName: cliOptions.target ?? 'default', @@ -492,6 +513,8 @@ Model & Runtime Overrides (harness only) [non-interactive] name: 'invoke', sessionId: cliOptions.sessionId, userId: cliOptions.userId, + endpoint, + endpointSource, headers, bearerToken: cliOptions.bearerToken, paymentInstrumentId: cliOptions.paymentInstrumentId, diff --git a/src/cli/commands/invoke/resolve.ts b/src/cli/commands/invoke/resolve.ts index 4b76c4a6d..2eb540634 100644 --- a/src/cli/commands/invoke/resolve.ts +++ b/src/cli/commands/invoke/resolve.ts @@ -16,6 +16,8 @@ export interface ResolveInvokeInput { awsTargets: AwsDeploymentTargets; agentName?: string; targetName?: string; + /** Named runtime endpoint (version alias) to target. When omitted, the DEFAULT endpoint is used. */ + endpointName?: string; bearerToken?: string; sessionId?: string; configIO?: ConfigIO; @@ -27,6 +29,8 @@ export interface ResolvedInvokeTarget { targetConfig: AwsDeploymentTarget; region: string; runtimeArn: string; + /** Resolved runtime endpoint qualifier (the endpoint NAME). Undefined targets the DEFAULT endpoint. */ + endpoint?: string; bearerToken?: string; sessionId?: string; baggage?: string; @@ -99,6 +103,39 @@ export async function resolveInvokeTarget(input: ResolveInvokeInput): Promise k.startsWith(`${agentSpec.name}/`)) + .map(k => k.slice(`${agentSpec.name}/`.length)), + ]) + ); + return { + success: false, + error: new ResourceNotFoundError( + `Endpoint '${input.endpointName}' not found for agent '${agentSpec.name}'.` + + (available.length > 0 ? ` Available: ${available.join(', ')}` : '') + ), + }; + } + endpoint = input.endpointName; + } + // Build config bundle baggage if a bundle is associated with this agent const deployedBundles = targetState?.resources?.configBundles ?? {}; let baggage: string | undefined; @@ -157,6 +194,7 @@ export async function resolveInvokeTarget(input: ResolveInvokeInput): Promise; bearerToken?: string; isResume?: boolean; @@ -248,6 +252,8 @@ function AppContent({ initialSessionId={route.sessionId} isResume={route.isResume} initialUserId={route.userId} + initialEndpoint={route.endpoint} + initialEndpointSource={route.endpointSource} initialHeaders={route.headers} initialBearerToken={route.bearerToken} onExec={result => { diff --git a/src/cli/tui/screens/invoke/InvokeScreen.tsx b/src/cli/tui/screens/invoke/InvokeScreen.tsx index 092076a1e..016638b43 100644 --- a/src/cli/tui/screens/invoke/InvokeScreen.tsx +++ b/src/cli/tui/screens/invoke/InvokeScreen.tsx @@ -14,6 +14,10 @@ interface InvokeScreenProps { initialPrompt?: string; initialSessionId?: string; initialUserId?: string; + /** Resolved named runtime endpoint (version alias) to target on every invocation. Undefined targets DEFAULT. */ + initialEndpoint?: string; + /** Where the resolved endpoint came from, for telemetry. */ + initialEndpointSource?: 'flag' | 'env' | 'default'; /** Custom headers to forward to the agent runtime on every invocation */ initialHeaders?: Record; initialBearerToken?: string; @@ -155,6 +159,8 @@ export function InvokeScreen({ initialPrompt, initialSessionId, initialUserId, + initialEndpoint, + initialEndpointSource, initialHeaders, initialBearerToken, onExec, @@ -189,6 +195,8 @@ export function InvokeScreen({ } = useInvokeFlow({ initialSessionId, initialUserId, + initialEndpoint, + initialEndpointSource, headers: initialHeaders, initialBearerToken, isResume, diff --git a/src/cli/tui/screens/invoke/useInvokeFlow.ts b/src/cli/tui/screens/invoke/useInvokeFlow.ts index fd01237b6..8fbbe9785 100644 --- a/src/cli/tui/screens/invoke/useInvokeFlow.ts +++ b/src/cli/tui/screens/invoke/useInvokeFlow.ts @@ -69,6 +69,10 @@ export interface InvokeConfig { export interface InvokeFlowOptions { initialSessionId?: string; initialUserId?: string; + /** Resolved named runtime endpoint (version alias) to target on every invocation. Undefined targets DEFAULT. */ + initialEndpoint?: string; + /** Where the resolved endpoint came from, for telemetry. */ + initialEndpointSource?: 'flag' | 'env' | 'default'; /** Custom headers to forward to the agent runtime on every invocation */ headers?: Record; initialBearerToken?: string; @@ -121,6 +125,8 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState const { initialSessionId, initialUserId, + initialEndpoint, + initialEndpointSource, headers, initialBearerToken, isResume, @@ -183,6 +189,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState hasSessionId: !!initialSessionId, bearerToken: initialBearerToken, agentProtocol: firstProtocol, + endpointSource: initialEndpointSource, }), async () => { if (!project) { @@ -668,6 +675,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState logger, headers, bearerToken: bearerToken || undefined, + endpoint: initialEndpoint, }, aguiInput ); @@ -790,6 +798,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState region: config.target.region, runtimeArn: agent.state.runtimeArn, payload: prompt, + endpoint: initialEndpoint, sessionId: sessionId ?? undefined, userId, logger, @@ -850,6 +859,7 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState fetchMcpTools, getMcpInvokeOptions, streamHarnessInvoke, + initialEndpoint, initialPaymentInstrumentId, initialPaymentUserId, ]