UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

173 lines • 7.72 kB
/** * The AgentCore Code Interpreter client seam: the operation-semantic surface * `agentCoreCodeRunner` calls, and its ONE mapping onto the real SDK. * * Two things this file owns and nothing else does: * * • the COMMAND FORM — a bare `@aws-sdk/client-*` client is command-based: * `client.send(new SomeCommand(input))`, never a method on the client * (the 9.4.0 law; `BedrockAgentCoreClient.prototype.startCodeInterpreterSession` * is `undefined`, the shortcut exists only on the aggregated client); * • the DRAIN of `InvokeCodeInterpreterResponse.stream`, an event stream * whose members are `{ result }` or a modelled EXCEPTION. * * The request and response shapes below were read off a real install of * `@aws-sdk/client-bedrock-agentcore` 3.1159.0 (`InvokeCodeInterpreterRequest`, * `ToolArguments`, `InputContentBlock`, `CodeInterpreterResult`, * `ResourceContent`, `ToolResultStructuredContent`) and are pinned against * transcribed copies of those types in * `test/type-regressions/AgentCoreCodeInterpreter.sdk.test.ts`. * * Pattern: Adapter (GoF) + lazy peer-dep load — the SDK is required only when * a session first starts (or never, if you inject `_client` / `_sdk`). */ import { abortableIterable } from '../../lib/abort.js'; import { lazyRequire } from '../../lib/lazyRequire.js'; /** The tool names whose answer is only an answer if it carried a result. */ const RESULT_REQUIRED = new Set([ 'writeFiles', 'readFiles', ]); /** * Map {@link AgentCoreCodeClientLike} onto the real SDK commands. */ export function createCodeClient(options) { let mod; if (options._sdk) { mod = options._sdk; } else { try { // Lazy peer-dep: only loaded when no _client/_sdk is injected and start() runs. mod = lazyRequire('@aws-sdk/client-bedrock-agentcore'); } catch { throw new Error('agentCoreCodeRunner requires the `@aws-sdk/client-bedrock-agentcore` peer dependency.\n' + ' Install: npm install @aws-sdk/client-bedrock-agentcore\n' + ' Or pass `_client` for a pre-built or mock client.\n' + ' For a local dev loop with no AWS at all, use `localCodeRunner()`.'); } } if (!mod.BedrockAgentCoreClient) { throw new Error('agentCoreCodeRunner: `@aws-sdk/client-bedrock-agentcore` is installed but ' + '`BedrockAgentCoreClient` was not found. Update the SDK.'); } const sdk = new mod.BedrockAgentCoreClient({ ...(options.region && { region: options.region }) }); const send = async (Ctor, name, input, abortSignal) => { if (!Ctor) { throw new Error(`agentCoreCodeRunner: \`@aws-sdk/client-bedrock-agentcore\` is missing ${name}. ` + 'Upgrade the SDK, or pass `_client` with your own mapping.'); } return abortSignal ? sdk.send(new Ctor(input), { abortSignal }) : sdk.send(new Ctor(input)); }; return { async startSession(input) { const r = (await send(mod.StartCodeInterpreterSessionCommand, 'StartCodeInterpreterSessionCommand', input)); return { ...(r?.sessionId !== undefined && { sessionId: r.sessionId }) }; }, async invoke(input, invokeOptions) { const abortSignal = invokeOptions?.abortSignal; const r = (await send(mod.InvokeCodeInterpreterCommand, 'InvokeCodeInterpreterCommand', input, abortSignal)); return drainStream(r?.stream, { requireResult: RESULT_REQUIRED.has(input.name), ...(abortSignal && { signal: abortSignal }), }); }, async stopSession(input) { await send(mod.StopCodeInterpreterSessionCommand, 'StopCodeInterpreterSessionCommand', input); }, }; } /** Every modelled error member of the stream union, in SDK spelling. */ export const STREAM_ERROR_MEMBERS = [ 'accessDeniedException', 'conflictException', 'internalServerException', 'resourceNotFoundException', 'serviceQuotaExceededException', 'throttlingException', 'validationException', ]; /** * Drain the event stream into one answer. * * An ERROR MEMBER RAISES. This is the honesty line for this adapter: the SDK * models seven failure shapes as ordinary members of the stream union, so an * `AccessDeniedException` arrives as a value, not a rejection. Folding it in * as empty output would report a clean run in which the code "printed * nothing" — a silent success, at the exact moment an operator most needs the * word "AccessDenied". * * Nothing is filled in that the stream did not carry: no exit status, no * error flag. And a file operation whose stream ended with no result at all * (`requireResult`) is a broken call, not an empty answer. */ export async function drainStream(stream, options) { if (!stream) { throw new Error('agentCoreCodeRunner: InvokeCodeInterpreter returned no stream. The response carries ' + 'its payload as an event stream (`response.stream`), so an absent one is a broken ' + 'call, not an empty result.'); } let stdout = ''; let stderr = ''; let exitCode; let isError; let results = 0; const resources = []; for await (const event of abortableIterable(stream, options.signal)) { for (const member of STREAM_ERROR_MEMBERS) { const raised = event[member]; if (raised) { const error = new Error(`agentCoreCodeRunner: ${member} from InvokeCodeInterpreter — ` + `${raised.message ?? 'no message supplied'}`); error.name = member.charAt(0).toUpperCase() + member.slice(1); throw error; } } const result = event.result; if (!result) continue; results += 1; if (result.isError === true) isError = true; else if (result.isError === false && isError === undefined) isError = false; for (const block of result.content ?? []) { if (block.resource !== undefined && block.resource !== null) resources.push(block.resource); } const structured = result.structuredContent; if (structured) { if (structured.stdout !== undefined) stdout += structured.stdout; if (structured.stderr !== undefined) stderr += structured.stderr; if (structured.exitCode !== undefined) exitCode = structured.exitCode; } else { // Fallback: a response that filled only the typed content blocks. Text // blocks are the readable half; a binary/resource block is described by // name rather than inlined, because inlining it is the bug this port exists // to prevent. for (const block of result.content ?? []) { if (block.text !== undefined) stdout += block.text; else if (block.name !== undefined) stdout += `[${block.type ?? 'resource'}: ${block.name}]`; } } } if (options.requireResult && results === 0) { throw new Error('agentCoreCodeRunner: InvokeCodeInterpreter answered a file operation with no result. ' + 'Nothing confirmed it, so it is not reported as done.'); } return { stdout, stderr, ...(exitCode !== undefined && { exitCode }), ...(isError !== undefined && { isError }), ...(resources.length > 0 && { resources }), }; } //# sourceMappingURL=agentcoreClient.js.map