docs: document shared package contracts

This commit is contained in:
Peter Steinberger
2026-06-04 01:39:12 -04:00
parent d14fe163b5
commit eaf803b223
13 changed files with 106 additions and 0 deletions

View File

@@ -1,3 +1,5 @@
// Public barrel for shared ACP session, metadata, and runtime helper contracts.
export * from "./error-format.js";
export * from "./meta.js";
export * from "./normalize-text.js";

View File

@@ -1 +1,3 @@
// ACP text normalization facade shared with older imports.
export { normalizeOptionalString as normalizeText } from "@openclaw/normalization-core/string-coerce";

View File

@@ -1 +1,3 @@
// ACP record normalization facade shared with older imports.
export { asOptionalRecord as asRecord } from "@openclaw/normalization-core/record-coerce";

View File

@@ -2,6 +2,9 @@ import { normalizeText } from "../normalize-text.js";
import type { SessionAcpIdentity, SessionAcpIdentitySource, SessionAcpMeta } from "../types.js";
import type { AcpRuntimeHandle, AcpRuntimeStatus } from "./types.js";
// ACP session identity merge and extraction helpers for resume-safe runtime state.
/** Normalize a stored identity state value from metadata. */
function normalizeIdentityState(value: unknown): SessionAcpIdentity["state"] | undefined {
if (value !== "pending" && value !== "resolved") {
return undefined;
@@ -9,6 +12,7 @@ function normalizeIdentityState(value: unknown): SessionAcpIdentity["state"] | u
return value;
}
/** Normalize where an ACP identity observation came from. */
function normalizeIdentitySource(value: unknown): SessionAcpIdentitySource | undefined {
if (value !== "ensure" && value !== "status" && value !== "event") {
return undefined;
@@ -16,6 +20,7 @@ function normalizeIdentitySource(value: unknown): SessionAcpIdentitySource | und
return value;
}
/** Normalize an identity object and infer pending/resolved state from stable ids. */
function normalizeIdentity(
identity: SessionAcpIdentity | undefined,
): SessionAcpIdentity | undefined {
@@ -49,6 +54,7 @@ function normalizeIdentity(
type IdentityIds = Pick<SessionAcpIdentity, "acpxRecordId" | "acpxSessionId" | "agentSessionId">;
/** Read identity ids from a runtime handle shape. */
function readIdentityIdsFromHandle(handle: AcpRuntimeHandle): IdentityIds {
return {
acpxRecordId: normalizeText((handle as { acpxRecordId?: unknown }).acpxRecordId),
@@ -57,6 +63,7 @@ function readIdentityIdsFromHandle(handle: AcpRuntimeHandle): IdentityIds {
};
}
/** Build an identity only when at least one stable id is known. */
function buildSessionIdentity(params: {
ids: IdentityIds;
state: SessionAcpIdentity["state"];
@@ -77,6 +84,7 @@ function buildSessionIdentity(params: {
};
}
/** Resolve normalized ACP identity from persisted session metadata. */
export function resolveSessionIdentityFromMeta(
meta: SessionAcpMeta | undefined,
): SessionAcpIdentity | undefined {
@@ -86,10 +94,12 @@ export function resolveSessionIdentityFromMeta(
return normalizeIdentity(meta.identity);
}
/** Return true when an identity has a backend or agent session id. */
export function identityHasStableSessionId(identity: SessionAcpIdentity | undefined): boolean {
return Boolean(identity?.acpxSessionId || identity?.agentSessionId);
}
/** Resolve the runtime resume id, preferring agent session id over ACP backend id. */
export function resolveRuntimeResumeSessionId(
identity: SessionAcpIdentity | undefined,
): string | undefined {
@@ -99,6 +109,7 @@ export function resolveRuntimeResumeSessionId(
return normalizeText(identity.agentSessionId) ?? normalizeText(identity.acpxSessionId);
}
/** Return true when identity is absent or still pending. */
export function isSessionIdentityPending(identity: SessionAcpIdentity | undefined): boolean {
if (!identity) {
return true;
@@ -106,6 +117,7 @@ export function isSessionIdentityPending(identity: SessionAcpIdentity | undefine
return identity.state === "pending";
}
/** Compare identities ignoring lastUpdatedAt timestamp churn. */
export function identityEquals(
left: SessionAcpIdentity | undefined,
right: SessionAcpIdentity | undefined,
@@ -127,6 +139,7 @@ export function identityEquals(
);
}
/** Merge current and incoming identity observations without downgrading resolved ids. */
export function mergeSessionIdentity(params: {
current: SessionAcpIdentity | undefined;
incoming: SessionAcpIdentity | undefined;
@@ -174,6 +187,7 @@ export function mergeSessionIdentity(params: {
return next;
}
/** Create a pending identity from an ensure-session handle. */
export function createIdentityFromEnsure(params: {
handle: AcpRuntimeHandle;
now: number;
@@ -186,6 +200,7 @@ export function createIdentityFromEnsure(params: {
});
}
/** Create an identity from a runtime event handle. */
export function createIdentityFromHandleEvent(params: {
handle: AcpRuntimeHandle;
now: number;
@@ -199,6 +214,7 @@ export function createIdentityFromHandleEvent(params: {
});
}
/** Create an identity from runtime status output. */
export function createIdentityFromStatus(params: {
status: AcpRuntimeStatus | undefined;
now: number;
@@ -230,6 +246,7 @@ export function createIdentityFromStatus(params: {
};
}
/** Convert ACP identity ids into runtime handle resume identifiers. */
export function resolveRuntimeHandleIdentifiersFromIdentity(
identity: SessionAcpIdentity | undefined,
): { backendSessionId?: string; agentSessionId?: string } {

View File

@@ -1,3 +1,5 @@
// Public barrel for media URL, MIME, path, and bounded-read helpers.
export * from "./base64.js";
export * from "./constants.js";
export * from "./content-length.js";

View File

@@ -1,14 +1,19 @@
import { normalizeProviderId } from "./provider-id.js";
// Collects configured model references from OpenClaw config-shaped objects.
/** Narrow unknown values to plain records. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** One configured model reference plus its config path. */
export type ConfiguredModelRef = {
path: string;
value: string;
};
/** Agent config keys that can contain direct model references. */
export const AGENT_MODEL_CONFIG_KEYS = [
"model",
"imageModel",
@@ -19,6 +24,7 @@ export const AGENT_MODEL_CONFIG_KEYS = [
"pdfModel",
] as const;
/** Collect configured model references from agents, channels, hooks, and message config. */
export function collectConfiguredModelRefs(
config: unknown,
options: { includeChannelModelOverrides?: boolean } = {},
@@ -117,6 +123,7 @@ export function collectConfiguredModelRefs(
return refs;
}
/** Collect only configured model reference values. */
export function collectConfiguredModelRefValues(
config: unknown,
options?: { includeChannelModelOverrides?: boolean },
@@ -124,6 +131,7 @@ export function collectConfiguredModelRefValues(
return collectConfiguredModelRefs(config, options).map((ref) => ref.value);
}
/** Extract a normalized provider id from a provider/model reference. */
export function extractProviderFromModelRef(value: string): string | null {
const trimmed = value.trim();
const slash = trimmed.indexOf("/");

View File

@@ -1,3 +1,5 @@
// Public barrel for model catalog normalization, ids, refs, and types.
export * from "./configured-model-refs.js";
export * from "./model-catalog-normalize.js";
export * from "./model-catalog-refs.js";

View File

@@ -26,6 +26,8 @@ import {
type NormalizedModelCatalogRow,
} from "./model-catalog-types.js";
// Normalizes raw provider model catalogs into stable rows for lookup and merging.
const MODEL_CATALOG_INPUTS = new Set(["text", "image", "document"]);
const MODEL_CATALOG_DISCOVERY_MODES = new Set(["static", "refreshable", "runtime"]);
const MODEL_CATALOG_STATUSES = new Set(["available", "preview", "deprecated", "disabled"]);
@@ -33,14 +35,17 @@ const MODEL_CATALOG_API_SET = new Set<string>(MODEL_CATALOG_APIS);
const DEFAULT_MODEL_INPUT: ModelCatalogInput[] = ["text"];
const DEFAULT_MODEL_STATUS: ModelCatalogStatus = "available";
/** Narrow unknown catalog payloads to plain records. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Reject object keys that can mutate prototypes when copied into records. */
function isBlockedObjectKey(key: string): boolean {
return key === "__proto__" || key === "prototype" || key === "constructor";
}
/** Normalize optional catalog strings. */
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
@@ -49,6 +54,7 @@ function normalizeOptionalString(value: unknown): string | undefined {
return trimmed ? trimmed : undefined;
}
/** Normalize arrays of trimmed strings, dropping invalid entries. */
function normalizeTrimmedStringList(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
@@ -638,6 +644,7 @@ function normalizeModelCatalogDiscovery(
return Object.keys(discovery).length > 0 ? discovery : undefined;
}
/** Normalize a raw model catalog object for the set of providers owned by a plugin/manifest. */
export function normalizeModelCatalog(
value: unknown,
params: { ownedProviders: ReadonlySet<string> },
@@ -661,6 +668,7 @@ export function normalizeModelCatalog(
return Object.keys(catalog).length > 0 ? catalog : undefined;
}
/** Normalize one provider catalog into sorted runtime rows. */
export function normalizeModelCatalogProviderRows(params: {
provider: string;
providerCatalog: ModelCatalogProvider;
@@ -722,6 +730,7 @@ export function normalizeModelCatalogProviderRows(params: {
return rows.toSorted((a, b) => a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id));
}
/** Normalize all provider catalogs into sorted runtime rows. */
export function normalizeModelCatalogRows(params: {
providers: Record<string, ModelCatalogProvider>;
source: ModelCatalogSource;

View File

@@ -1,13 +1,18 @@
import { normalizeLowercaseStringOrEmpty } from "./provider-id.js";
// Stable model catalog ref and merge-key builders.
/** Normalize provider ids for catalog refs. */
export function normalizeModelCatalogProviderId(provider: string): string {
return normalizeLowercaseStringOrEmpty(provider);
}
/** Build a provider/model catalog reference. */
export function buildModelCatalogRef(provider: string, modelId: string): string {
return `${normalizeModelCatalogProviderId(provider)}/${modelId}`;
}
/** Build a case-insensitive merge key for provider/model rows. */
export function buildModelCatalogMergeKey(provider: string, modelId: string): string {
return `${normalizeModelCatalogProviderId(provider)}::${normalizeLowercaseStringOrEmpty(modelId)}`;
}

View File

@@ -1,3 +1,6 @@
// Shared model catalog data contracts for provider manifests and normalized rows.
/** Supported API protocols for model catalog entries. */
export const MODEL_CATALOG_APIS = [
"openai-completions",
"openai-responses",
@@ -11,8 +14,10 @@ export const MODEL_CATALOG_APIS = [
"azure-openai-responses",
] as const;
/** API protocol for a model catalog entry. */
export type ModelCatalogApi = (typeof MODEL_CATALOG_APIS)[number];
/** Supported model thinking/reasoning wire formats. */
export const MODEL_CATALOG_THINKING_FORMATS = [
"openai",
"openrouter",
@@ -23,12 +28,15 @@ export const MODEL_CATALOG_THINKING_FORMATS = [
"zai",
] as const;
/** Thinking/reasoning wire format for model compatibility. */
export type ModelCatalogThinkingFormat = (typeof MODEL_CATALOG_THINKING_FORMATS)[number];
/** Narrow a string to a supported model catalog thinking format. */
export function isModelCatalogThinkingFormat(value: string): value is ModelCatalogThinkingFormat {
return (MODEL_CATALOG_THINKING_FORMATS as readonly string[]).includes(value);
}
/** Compatibility flags and provider-specific routing metadata for one model. */
export type ModelCatalogCompatConfig = {
supportsStore?: boolean;
supportsDeveloperRole?: boolean;
@@ -63,6 +71,7 @@ export type ModelCatalogCompatConfig = {
visibleReasoningDetailTypes?: string[];
};
/** OpenRouter routing preferences copied into request metadata. */
export type ModelCatalogOpenRouterRouting = {
allow_fallbacks?: boolean;
require_parameters?: boolean;
@@ -104,11 +113,13 @@ export type ModelCatalogOpenRouterRouting = {
};
};
/** Vercel AI Gateway routing preferences. */
export type ModelCatalogVercelGatewayRouting = {
only?: string[];
order?: string[];
};
/** Image input limits for a model. */
export type ModelCatalogImageInputConfig = {
maxBytes?: number;
maxPixels?: number;
@@ -117,13 +128,18 @@ export type ModelCatalogImageInputConfig = {
tokenMode?: "tile" | "detail" | "provider";
};
/** Media input limits for a model. */
export type ModelCatalogMediaInputConfig = {
image?: ModelCatalogImageInputConfig;
};
/** Supported input modality for a model. */
export type ModelCatalogInput = "text" | "image" | "document";
/** Discovery lifecycle for a provider catalog. */
export type ModelCatalogDiscovery = "static" | "refreshable" | "runtime";
/** Availability state for a model. */
export type ModelCatalogStatus = "available" | "preview" | "deprecated" | "disabled";
/** Source of a model catalog row. */
export type ModelCatalogSource =
| "manifest"
| "provider-index"
@@ -131,6 +147,7 @@ export type ModelCatalogSource =
| "config"
| "runtime-refresh";
/** Unified catalog kind across text and generated media models. */
export type UnifiedModelCatalogKind =
| "text"
| "voice"
@@ -138,6 +155,7 @@ export type UnifiedModelCatalogKind =
| "video_generation"
| "music_generation";
/** Source for unified model catalog entries. */
export type UnifiedModelCatalogSource =
| "manifest"
| "provider-index"
@@ -147,6 +165,7 @@ export type UnifiedModelCatalogSource =
| "configured"
| "runtime-refresh";
/** Unified model catalog entry for provider/model pickers. */
export type UnifiedModelCatalogEntry<TCapabilities = unknown> = {
kind: UnifiedModelCatalogKind;
provider: string;
@@ -164,6 +183,7 @@ export type UnifiedModelCatalogEntry<TCapabilities = unknown> = {
warnings?: readonly string[];
};
/** Tiered token cost row. */
export type ModelCatalogTieredCost = {
input: number;
output: number;
@@ -172,6 +192,7 @@ export type ModelCatalogTieredCost = {
range: [number, number] | [number];
};
/** Token cost metadata for one model. */
export type ModelCatalogCost = {
input?: number;
output?: number;
@@ -180,6 +201,7 @@ export type ModelCatalogCost = {
tieredPricing?: ModelCatalogTieredCost[];
};
/** Provider manifest model entry. */
export type ModelCatalogModel = {
id: string;
name?: string;
@@ -201,6 +223,7 @@ export type ModelCatalogModel = {
tags?: string[];
};
/** Provider manifest catalog entry. */
export type ModelCatalogProvider = {
baseUrl?: string;
api?: ModelCatalogApi;
@@ -208,12 +231,14 @@ export type ModelCatalogProvider = {
models: ModelCatalogModel[];
};
/** Provider alias entry. */
export type ModelCatalogAlias = {
provider: string;
api?: ModelCatalogApi;
baseUrl?: string;
};
/** Suppression rule for hiding a provider/model under matching config. */
export type ModelCatalogSuppression = {
provider: string;
model: string;
@@ -224,6 +249,7 @@ export type ModelCatalogSuppression = {
};
};
/** Raw model catalog manifest shape. */
export type ModelCatalog = {
providers?: Record<string, ModelCatalogProvider>;
aliases?: Record<string, ModelCatalogAlias>;
@@ -232,6 +258,7 @@ export type ModelCatalog = {
runtimeAugment?: boolean;
};
/** Normalized model catalog row used by runtime lookup and UI surfaces. */
export type NormalizedModelCatalogRow = {
provider: string;
id: string;

View File

@@ -4,6 +4,9 @@ import {
normalizeTogetherModelId,
} from "./provider-model-id-normalize.js";
// Provider model-id normalization policies from manifests plus built-in provider rules.
/** Manifest-defined normalization rules for one provider. */
export type ManifestModelIdNormalizationProvider = {
aliases?: Record<string, string>;
stripPrefixes?: string[];
@@ -14,6 +17,7 @@ export type ManifestModelIdNormalizationProvider = {
}[];
};
/** Manifest fragment that can define provider model-id normalization policies. */
export type ManifestModelIdNormalizationRecord = {
modelIdNormalization?: {
providers?: Record<string, ManifestModelIdNormalizationProvider>;
@@ -24,6 +28,7 @@ let currentManifestModelIdNormalizationPolicies:
| ReadonlyMap<string, ManifestModelIdNormalizationProvider>
| undefined;
/** Collect provider model-id normalization policies from plugin manifests. */
export function collectManifestModelIdNormalizationPolicies(
plugins: readonly ManifestModelIdNormalizationRecord[],
): Map<string, ManifestModelIdNormalizationProvider> {
@@ -36,6 +41,7 @@ export function collectManifestModelIdNormalizationPolicies(
return policies;
}
/** Replace the process-local manifest normalization policy snapshot. */
export function setCurrentManifestModelIdNormalizationRecords(
plugins: readonly ManifestModelIdNormalizationRecord[] | undefined,
): void {
@@ -44,20 +50,24 @@ export function setCurrentManifestModelIdNormalizationRecords(
: undefined;
}
/** Return the current process-local manifest normalization policy snapshot. */
export function getCurrentManifestModelIdNormalizationPolicies():
| ReadonlyMap<string, ManifestModelIdNormalizationProvider>
| undefined {
return currentManifestModelIdNormalizationPolicies;
}
/** Return true when a model id already includes a provider namespace. */
function hasProviderPrefix(modelId: string): boolean {
return modelId.includes("/");
}
/** Join a provider prefix and model id with exactly one slash. */
function formatPrefixedModelId(prefix: string, modelId: string): string {
return `${prefix.replace(/\/+$/u, "")}/${modelId.replace(/^\/+/u, "")}`;
}
/** Strip a duplicated self-provider prefix from a model id. */
export function stripSelfProviderModelPrefix(provider: string, model: string): string {
const prefix = `${normalizeLowercaseStringOrEmpty(provider)}/`;
const trimmed = model.trim();
@@ -66,6 +76,7 @@ export function stripSelfProviderModelPrefix(provider: string, model: string): s
: model;
}
/** Apply manifest normalization policies for one provider/model id. */
export function normalizeProviderModelIdWithPolicies(params: {
provider: string;
policies: ReadonlyMap<string, ManifestModelIdNormalizationProvider>;
@@ -107,6 +118,7 @@ export function normalizeProviderModelIdWithPolicies(params: {
return modelId;
}
/** Apply built-in provider-specific model id normalization rules. */
export function normalizeBuiltInProviderModelId(provider: string, model: string): string {
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
if (
@@ -174,6 +186,7 @@ export function normalizeBuiltInProviderModelId(provider: string, model: string)
return model;
}
/** Apply manifest policies and built-in normalization to a static provider/model id. */
export function normalizeStaticProviderModelIdWithPolicies(
provider: string,
model: string,
@@ -192,6 +205,7 @@ export function normalizeStaticProviderModelIdWithPolicies(
return normalizeBuiltInProviderModelId(normalizedProvider, manifestModelId);
}
/** Normalize a configured provider/model catalog reference using current policies. */
export function normalizeConfiguredProviderCatalogModelId(
provider: string,
model: string,
@@ -201,6 +215,7 @@ export function normalizeConfiguredProviderCatalogModelId(
return normalizeConfiguredProviderCatalogModelRef(providerModel);
}
/** Normalize embedded Google model aliases inside provider/model catalog refs. */
export function normalizeConfiguredProviderCatalogModelRef(providerModel: string): string {
const googlePrefix = "google/";
if (!providerModel.startsWith(googlePrefix)) {

View File

@@ -1,3 +1,5 @@
// Public barrel for shared coercion and normalization helpers.
export * from "./number-coercion.js";
export * from "./record-coerce.js";
export * from "./string-coerce.js";

View File

@@ -1,5 +1,9 @@
// External code plugin package.json compatibility and validation contracts.
/** JSON object shape accepted by package contract helpers. */
export type JsonObject = Record<string, unknown>;
/** Compatibility metadata extracted from an external plugin package. */
export type ExternalPluginCompatibility = {
pluginApiRange?: string;
builtWithOpenClawVersion?: string;
@@ -7,25 +11,30 @@ export type ExternalPluginCompatibility = {
minGatewayVersion?: string;
};
/** One validation issue for an external plugin package. */
export type ExternalPluginValidationIssue = {
fieldPath: string;
message: string;
};
/** Validation result plus any normalized compatibility metadata. */
export type ExternalCodePluginValidationResult = {
compatibility?: ExternalPluginCompatibility;
issues: ExternalPluginValidationIssue[];
};
/** Required package.json field paths for external code plugin packages. */
export const EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [
"openclaw.compat.pluginApi",
"openclaw.build.openclawVersion",
] as const;
/** Narrow unknown values to plain records. */
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Normalize optional package metadata strings. */
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
@@ -34,6 +43,7 @@ function normalizeOptionalString(value: unknown): string | undefined {
return trimmed ? trimmed : undefined;
}
/** Read OpenClaw package.json blocks without trusting caller input shape. */
function readOpenClawBlock(packageJson: unknown) {
const root = isRecord(packageJson) ? packageJson : undefined;
const openclaw = isRecord(root?.openclaw) ? root.openclaw : undefined;
@@ -43,6 +53,7 @@ function readOpenClawBlock(packageJson: unknown) {
return { root, openclaw, compat, build, install };
}
/** Normalize compatibility metadata from an external plugin package.json. */
export function normalizeExternalPluginCompatibility(
packageJson: unknown,
): ExternalPluginCompatibility | undefined {
@@ -74,6 +85,7 @@ export function normalizeExternalPluginCompatibility(
return Object.keys(compatibility).length > 0 ? compatibility : undefined;
}
/** List missing required field paths for an external code plugin package.json. */
export function listMissingExternalCodePluginFieldPaths(packageJson: unknown): string[] {
const { compat, build } = readOpenClawBlock(packageJson);
const missing: string[] = [];
@@ -86,6 +98,7 @@ export function listMissingExternalCodePluginFieldPaths(packageJson: unknown): s
return missing;
}
/** Validate an external code plugin package.json against required compatibility fields. */
export function validateExternalCodePluginPackageJson(
packageJson: unknown,
): ExternalCodePluginValidationResult {