The bridge counted tool and model calls against fixed 100 and 50 caps, so CONFLUENCE_WEB_MAX_CONFLUENCE_CALLS / CONFLUENCE_WEB_MAX_MODEL_CALLS above the defaults were cut short inside the container, and the model cap failed the run outright. The backend now passes both values into the container at start, the supervisor forwards them to the bridge, and LIMITS reads them with the same 1-1000 validation as the backend, falling back to 100/50 when absent or invalid. Add a test that loads LIMITS in fresh processes with good and bad values for both budgets and the deadline.
277 lines
6.4 KiB
TypeScript
277 lines
6.4 KiB
TypeScript
/**
|
|
* Protocol types and limits defined in CONTRACTS.md Revision 1
|
|
*/
|
|
|
|
export const PROTOCOL_VERSION = 1;
|
|
|
|
// Size limits in bytes
|
|
export const KIB = 1024;
|
|
export const MIB = 1024 * 1024;
|
|
|
|
/**
|
|
* Protocol maximum for one query in milliseconds. The backend sets
|
|
* CONFLUENCE_WEB_MAX_DEADLINE_SECONDS on the container; the supervisor forwards
|
|
* it. Invalid or absent values fall back to 900 s (15 minutes).
|
|
*/
|
|
function maxDeadlineMs(): number {
|
|
const raw = typeof process !== 'undefined' ? process.env.CONFLUENCE_WEB_MAX_DEADLINE_SECONDS : undefined;
|
|
const seconds = raw === undefined || raw === '' ? NaN : Number(raw);
|
|
return Number.isInteger(seconds) && seconds >= 60 && seconds <= 3600 ? seconds * 1000 : 900_000;
|
|
}
|
|
|
|
/**
|
|
* Per-query call budget set by the backend at container start (mirrors its Settings value).
|
|
* Absent or invalid values fall back to the contract default so the container cannot be
|
|
* talked into an unbounded budget.
|
|
*/
|
|
function callBudget(name: string, fallback: number): number {
|
|
const raw = typeof process !== 'undefined' ? process.env[name] : undefined;
|
|
const value = raw === undefined || raw === '' ? NaN : Number(raw);
|
|
return Number.isInteger(value) && value >= 1 && value <= 1000 ? value : fallback;
|
|
}
|
|
|
|
export const LIMITS = {
|
|
USER_PROMPT_MAX_BYTES: 16 * MIB,
|
|
FINAL_MARKDOWN_MAX_BYTES: 128 * MIB,
|
|
TOOL_REQUEST_PAYLOAD_MAX_BYTES: 16 * MIB,
|
|
TOOL_RESULT_PAYLOAD_MAX_BYTES: 128 * MIB,
|
|
MODEL_REQUEST_PAYLOAD_MAX_BYTES: 128 * MIB,
|
|
MODEL_RESULT_PAYLOAD_MAX_BYTES: 128 * MIB,
|
|
ENVELOPE_ALLOWANCE_BYTES: 64 * KIB,
|
|
START_FRAME_MAX_BYTES: 6 * (16 * MIB) + 64 * KIB,
|
|
COLLECTION_START_FRAME_MAX_BYTES: 6 * (128 * MIB) + 64 * KIB,
|
|
ORDINARY_FRAME_MAX_BYTES: 128 * MIB + 64 * KIB,
|
|
ARTIFACT_CHUNK_MAX_DECODED_BYTES: 64 * KIB,
|
|
ARTIFACT_MAX_FILES: 20,
|
|
ARTIFACT_MAX_FILE_BYTES: 10 * MIB,
|
|
ARTIFACT_MAX_TOTAL_BYTES: 50 * MIB,
|
|
MAX_DEADLINE_MS: maxDeadlineMs(),
|
|
MAX_TOOL_CALLS: callBudget('CONFLUENCE_WEB_MAX_CONFLUENCE_CALLS', 100),
|
|
MAX_MODEL_CALLS: callBudget('CONFLUENCE_WEB_MAX_MODEL_CALLS', 50),
|
|
MAX_REMOTE_CONCURRENCY: 4,
|
|
MAX_WARNINGS: 100,
|
|
MAX_ERROR_MESSAGE_BYTES: 1024,
|
|
MAX_PATH_BYTES: 1024
|
|
} as const;
|
|
|
|
export interface ErrorShape {
|
|
code: string;
|
|
message: string;
|
|
}
|
|
|
|
export interface Warning {
|
|
code: string;
|
|
message: string;
|
|
tool_call_id?: string;
|
|
name?: string;
|
|
}
|
|
|
|
export interface Page {
|
|
page_id: string;
|
|
title: string;
|
|
space: string;
|
|
url: string;
|
|
}
|
|
|
|
export interface Pagination {
|
|
offset: number;
|
|
limit: number;
|
|
has_more: boolean;
|
|
}
|
|
|
|
export interface ModelDescriptor {
|
|
id: string;
|
|
context_window_tokens: number;
|
|
max_output_tokens: number;
|
|
}
|
|
|
|
// Model Protocol
|
|
export type TextContentBlock = {
|
|
type: "text";
|
|
text: string;
|
|
};
|
|
|
|
export type ToolCallContentBlock = {
|
|
type: "tool_call";
|
|
id: string;
|
|
name: string;
|
|
arguments: Record<string, any>;
|
|
};
|
|
|
|
export type ContentBlock = TextContentBlock | ToolCallContentBlock;
|
|
|
|
export interface UserMessageContract {
|
|
role: "user";
|
|
content: Array<{ type: "text"; text: string }>;
|
|
}
|
|
|
|
export interface AssistantMessageContract {
|
|
role: "assistant";
|
|
content: ContentBlock[];
|
|
provider_state?: string;
|
|
}
|
|
|
|
export interface ToolMessageContract {
|
|
role: "tool";
|
|
tool_call_id: string;
|
|
name: string;
|
|
content: string;
|
|
is_error: boolean;
|
|
}
|
|
|
|
export type MessageContract =
|
|
| UserMessageContract
|
|
| AssistantMessageContract
|
|
| ToolMessageContract;
|
|
|
|
export interface ToolDeclaration {
|
|
name: string;
|
|
description: string;
|
|
input_schema: Record<string, any>;
|
|
}
|
|
|
|
export interface ModelRequest {
|
|
messages: MessageContract[];
|
|
tools: ToolDeclaration[];
|
|
}
|
|
|
|
export interface ModelResponse {
|
|
content: ContentBlock[];
|
|
stop_reason: "stop" | "tool_calls" | "length";
|
|
usage: {
|
|
input_tokens: number;
|
|
output_tokens: number;
|
|
};
|
|
provider_state?: string;
|
|
}
|
|
|
|
// Bridge Protocol Frames
|
|
export interface BridgeBaseMessage {
|
|
v: number;
|
|
type: string;
|
|
id: string;
|
|
reply_to?: string;
|
|
payload: any;
|
|
}
|
|
|
|
export interface StartMessage extends BridgeBaseMessage {
|
|
type: "start";
|
|
payload: {
|
|
prompt: string;
|
|
system_instruction: string;
|
|
remaining_ms: number;
|
|
model: ModelDescriptor;
|
|
};
|
|
}
|
|
|
|
export interface ToolRequestMessage extends BridgeBaseMessage {
|
|
type: "tool_request";
|
|
payload: {
|
|
tool: string;
|
|
parameters: Record<string, any>;
|
|
};
|
|
}
|
|
|
|
export interface ToolResponseMessage extends BridgeBaseMessage {
|
|
type: "tool_response";
|
|
reply_to: string;
|
|
payload: {
|
|
result: any;
|
|
error: ErrorShape | null;
|
|
};
|
|
}
|
|
|
|
export interface ModelRequestMessage extends BridgeBaseMessage {
|
|
type: "model_request";
|
|
payload: ModelRequest;
|
|
}
|
|
|
|
export interface ModelResponseMessage extends BridgeBaseMessage {
|
|
type: "model_response";
|
|
reply_to: string;
|
|
payload: {
|
|
result: ModelResponse | null;
|
|
error: ErrorShape | null;
|
|
};
|
|
}
|
|
|
|
export interface CollectionStartMessage extends BridgeBaseMessage {
|
|
type: "collection_start";
|
|
payload: {
|
|
markdown: string;
|
|
warnings: Warning[];
|
|
};
|
|
}
|
|
|
|
export interface CollectionReadyMessage extends BridgeBaseMessage {
|
|
type: "collection_ready";
|
|
reply_to: string;
|
|
payload: Record<string, never>;
|
|
}
|
|
|
|
export interface ArtifactBeginMessage extends BridgeBaseMessage {
|
|
type: "artifact_begin";
|
|
payload: {
|
|
transfer_id: string;
|
|
name: string;
|
|
size_bytes: number;
|
|
};
|
|
}
|
|
|
|
export interface ArtifactAckMessage extends BridgeBaseMessage {
|
|
type: "artifact_ack";
|
|
reply_to: string;
|
|
payload: {
|
|
transfer_id: string;
|
|
decision: "accept" | "skip" | "stored";
|
|
warning: Warning | null;
|
|
};
|
|
}
|
|
|
|
export interface ArtifactChunkMessage extends BridgeBaseMessage {
|
|
type: "artifact_chunk";
|
|
payload: {
|
|
transfer_id: string;
|
|
index: number;
|
|
data_base64: string;
|
|
};
|
|
}
|
|
|
|
export interface ArtifactEndMessage extends BridgeBaseMessage {
|
|
type: "artifact_end";
|
|
payload: {
|
|
transfer_id: string;
|
|
size_bytes: number;
|
|
chunks: number;
|
|
};
|
|
}
|
|
|
|
export interface CompleteMessage extends BridgeBaseMessage {
|
|
type: "complete";
|
|
payload: {
|
|
accepted_transfer_count: number;
|
|
};
|
|
}
|
|
|
|
export interface ErrorMessage extends BridgeBaseMessage {
|
|
type: "error";
|
|
payload: ErrorShape;
|
|
}
|
|
|
|
export type BridgeInboundMessage =
|
|
| StartMessage
|
|
| ToolResponseMessage
|
|
| ModelResponseMessage
|
|
| CollectionReadyMessage
|
|
| ArtifactAckMessage;
|
|
|
|
export type BridgeOutboundMessage =
|
|
| ToolRequestMessage
|
|
| ModelRequestMessage
|
|
| CollectionStartMessage
|
|
| ArtifactBeginMessage
|
|
| ArtifactChunkMessage
|
|
| ArtifactEndMessage
|
|
| CompleteMessage
|
|
| ErrorMessage;
|