agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
115 lines • 5.78 kB
JavaScript
;
/**
* adapters/hosting/foundryResponses — an agent behind Microsoft Foundry's
* hosted-agent contract.
*
* ── The split, and why it is here ────────────────────────────────────────────
* Two different things are needed to serve a Foundry hosted agent, and only one
* of them is Microsoft's:
*
* • The **Responses protocol** — `input` items, a `response` object, the
* named streaming lifecycle. That is a protocol several runtimes speak, so
* it lives in `responsesWire.ts` with no vendor in it.
* • The **hosting contract** — port 8088, `POST /responses`, a `HEAD` probe
* on the same path, `GET /readiness` answering `{"status":"healthy"}`, and
* `agent_session_id` as a session alias. That IS this runtime's contract,
* the same way `/invocations` and `X-Amzn-…` are another's, and it is what
* this file supplies.
*
* So this file is a configuration, not an implementation. Everything hard —
* draining on close, aborting when the caller hangs up, failing a handler that
* throws or answers nothing — is `httpHost`'s, shared with every other adapter
* rather than reimplemented here where the two would drift.
*
* ── What it does NOT do ──────────────────────────────────────────────────────
* **Workflow Visualizer topology is not provided.** The Inspector can invoke
* this host and render its answer; it does not learn that the answer came from
* four composed agents, because nothing in the Responses protocol carries that
* and inventing a channel for it would be claiming a compatibility this has
* never demonstrated. An agent's internal structure is readable from
* agentfootprint's own recorders.
*
* Everything the protocol does not carry — tool calls, image and file input,
* function-call output, structured output — is refused by name. See
* `responsesWire.ts`.
*
* This is an **inbound hosting adapter**: it is the door callers arrive at. It
* is not a model provider, and it has nothing to do with which model the agent
* calls — for Foundry models, that is `openai()` with the endpoint's base URL.
*
* @example An agent behind the Inspector
* import { foundryResponsesHost, memorySessions, standingAgent } from 'agentfootprint/hosting';
*
* const handle = await standingAgent({
* agent,
* sessions: memorySessions(),
* host: foundryResponsesHost(),
* });
* process.on('SIGTERM', () => void handle.close());
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.foundryResponsesHost = exports.FOUNDRY_SESSION_FIELDS = exports.FOUNDRY_READINESS_PATH = exports.FOUNDRY_INVOKE_PATH = exports.DEFAULT_FOUNDRY_PORT = void 0;
const doorGuard_js_1 = require("../../hosting/doorGuard.js");
const httpHost_js_1 = require("../../hosting/httpHost.js");
const responsesWire_js_1 = require("./responsesWire.js");
/** The port a Foundry hosted agent is expected on. */
exports.DEFAULT_FOUNDRY_PORT = 8088;
/** The path that takes a turn. */
exports.FOUNDRY_INVOKE_PATH = '/responses';
/** The path a Foundry hosted agent answers a readiness probe on. */
exports.FOUNDRY_READINESS_PATH = '/readiness';
/**
* Session aliases this contract accepts, in precedence order.
*
* `conversation` is the protocol's own; `agent_session_id` is this runtime's;
* `session_id` is the spelling several clients send anyway. First one present
* wins, so a caller using any of the three reaches the same session.
*/
exports.FOUNDRY_SESSION_FIELDS = [
'conversation',
'agent_session_id',
'session_id',
];
/**
* One mebibyte: large enough for any resume or job description this door was
* built to carry, small enough that a hostile caller cannot spend the heap.
*/
const DEFAULT_MAX_BODY_BYTES = 1_048_576;
/**
* An {@link HttpHost} speaking Foundry's hosted-agent contract.
*
* @param options - Port, interface, model label and body ceiling.
*/
function foundryResponsesHost(options = {}) {
const { server, onUnhandled, model, hostname } = options;
// `port` and `hostname` name a socket a caller-owned server already bound, so
// they are passed only when this host is the one binding. Defaulting the port
// unconditionally would turn "attach to my server" into a refusal.
const socket = server
? { server }
: {
port: options.port ?? exports.DEFAULT_FOUNDRY_PORT,
...(hostname === undefined ? {} : { hostname }),
...(onUnhandled === undefined ? {} : { onUnhandled }),
};
return (0, httpHost_js_1.httpHost)({
name: 'foundryResponsesHost',
wire: (0, responsesWire_js_1.responsesWire)({
...(model === undefined ? {} : { defaultModel: model }),
sessionFields: exports.FOUNDRY_SESSION_FIELDS,
health: { status: 'healthy' },
}),
invokePath: exports.FOUNDRY_INVOKE_PATH,
healthPath: exports.FOUNDRY_READINESS_PATH,
// The Inspector asks whether the door is there before it uses it.
invokeHeadProbe: true,
maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
// The platform fronts this port: browser rules off unless the deployment
// sets them (see the options' doc) — except on a LOOPBACK bind, which no
// platform fronts, where every plain-host default applies.
...(0, doorGuard_js_1.platformFrontedDoor)(options, server === undefined ? hostname : undefined),
...socket,
});
}
exports.foundryResponsesHost = foundryResponsesHost;
//# sourceMappingURL=foundryResponses.js.map