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
JavaScript
/**
* 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