docs: document runtime helper contracts

This commit is contained in:
Peter Steinberger
2026-06-04 06:40:26 -04:00
parent fbf3e009d4
commit e533ff4c4a
16 changed files with 106 additions and 14 deletions
+12
View File
@@ -1,3 +1,8 @@
/**
* Channel-owned agent tool and prompt helpers.
* Discovers channel tools, message actions, prompt capabilities, reaction
* guidance, and weakly-attached channel metadata for wrapped tools.
*/
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
import { getChannelPlugin, listChannelPlugins } from "../channels/plugins/index.js";
import {
@@ -36,10 +41,12 @@ type ChannelMessageActionDiscoveryParams = {
const channelAgentToolMeta = new WeakMap<ChannelAgentTool, ChannelAgentToolMeta>();
/** Read channel metadata attached to a channel-owned agent tool. */
export function getChannelAgentToolMeta(tool: ChannelAgentTool): ChannelAgentToolMeta | undefined {
return channelAgentToolMeta.get(tool);
}
/** Copy channel metadata when wrapping or replacing a channel-owned tool. */
export function copyChannelAgentToolMeta(source: ChannelAgentTool, target: ChannelAgentTool): void {
const meta = channelAgentToolMeta.get(source);
if (meta) {
@@ -96,6 +103,7 @@ export function listAllChannelSupportedActions(
return Array.from(actions);
}
/** List agent tools contributed by registered channel plugins. */
export function listChannelAgentTools(params: { cfg?: OpenClawConfig }): ChannelAgentTool[] {
// Channel docking: aggregate channel-owned tools (login, etc.).
const tools: ChannelAgentTool[] = [];
@@ -115,6 +123,7 @@ export function listChannelAgentTools(params: { cfg?: OpenClawConfig }): Channel
return tools;
}
/** Resolve channel-specific message tool hints for system prompt assembly. */
export function resolveChannelMessageToolHints(params: {
cfg?: OpenClawConfig;
channel?: string | null;
@@ -132,6 +141,7 @@ export function resolveChannelMessageToolHints(params: {
return normalizeStringEntries(resolve({ cfg, accountId: params.accountId }));
}
/** Resolve channel prompt capabilities, including native approval UI support. */
export function resolveChannelPromptCapabilities(params: {
cfg?: OpenClawConfig;
channel?: string | null;
@@ -156,6 +166,7 @@ function normalizePromptCapabilities(capabilities?: readonly string[] | null): s
return normalizeStringEntries(capabilities ?? []);
}
/** Resolve optional channel reaction guidance for assistant replies. */
export function resolveChannelReactionGuidance(params: {
cfg?: OpenClawConfig;
channel?: string | null;
@@ -180,6 +191,7 @@ export function resolveChannelReactionGuidance(params: {
};
}
/** Test-only utilities for channel tool discovery state. */
export const testing = {
resetLoggedListActionErrors() {
messageActionTesting.resetLoggedMessageActionErrors();
+4
View File
@@ -1,3 +1,7 @@
/**
* Regression coverage for DeepSeek DSML streamed text filtering.
* Verifies complete, split, full-width, and unterminated DSML markup handling.
*/
import { describe, expect, it } from "vitest";
import { createDeepSeekTextFilter } from "./deepseek-text-filter.js";
+5 -2
View File
@@ -1,8 +1,11 @@
/**
* DeepSeek DSML streaming text filter.
* Removes provider-emitted DSML tool markup while buffering split tag prefixes
* across streamed chunks.
*/
const DSML_KINDS = ["tool_use_error", "tool_calls", "tool_call", "function_calls"] as const;
const DSML_BARS = ["|", ""] as const;
// Streaming filter for DeepSeek DSML tool markup. It removes complete markup
// blocks while holding enough trailing bytes to recognize split open/close tags.
const DSML_OPEN_TOKENS = DSML_BARS.flatMap((bar) =>
DSML_KINDS.map((kind) => `<${bar}DSML${bar}${kind}>`),
);
@@ -1,3 +1,7 @@
/**
* Regression coverage for internal runtime-context stripping and extraction.
* Verifies protected delimiters, legacy blocks, and custom-message filtering.
*/
import { describe, expect, it } from "vitest";
import {
escapeInternalRuntimeContextDelimiters,
+16
View File
@@ -1,14 +1,25 @@
/**
* Internal runtime-context delimiter and stripping helpers.
* Protects runtime-generated prompt blocks from user text and removes old
* context formats before replaying or comparing messages.
*/
/** Opening delimiter for protected OpenClaw runtime context blocks. */
export const INTERNAL_RUNTIME_CONTEXT_BEGIN = "<<<BEGIN_OPENCLAW_INTERNAL_CONTEXT>>>";
/** Closing delimiter for protected OpenClaw runtime context blocks. */
export const INTERNAL_RUNTIME_CONTEXT_END = "<<<END_OPENCLAW_INTERNAL_CONTEXT>>>";
const ESCAPED_INTERNAL_RUNTIME_CONTEXT_BEGIN = "[[OPENCLAW_INTERNAL_CONTEXT_BEGIN]]";
const ESCAPED_INTERNAL_RUNTIME_CONTEXT_END = "[[OPENCLAW_INTERNAL_CONTEXT_END]]";
/** Notice inserted into runtime-generated context blocks. */
export const OPENCLAW_RUNTIME_CONTEXT_NOTICE =
"This context is runtime-generated, not user-authored. Keep internal details private.";
/** Header for context attached to the immediately preceding user message. */
export const OPENCLAW_NEXT_TURN_RUNTIME_CONTEXT_HEADER =
"OpenClaw runtime context for the immediately preceding user message.";
/** Header for runtime events passed as prompt context. */
export const OPENCLAW_RUNTIME_EVENT_HEADER = "OpenClaw runtime event.";
/** Custom message type used for structured runtime-context messages. */
export const OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE = "openclaw.runtime-context";
const LEGACY_INTERNAL_CONTEXT_HEADER =
@@ -19,6 +30,7 @@ const LEGACY_INTERNAL_EVENT_SEPARATOR = "\n\n---\n\n";
const LEGACY_UNTRUSTED_RESULT_BEGIN = "<<<BEGIN_UNTRUSTED_CHILD_RESULT>>>";
const LEGACY_UNTRUSTED_RESULT_END = "<<<END_UNTRUSTED_CHILD_RESULT>>>";
/** Escape protected context delimiters before embedding untrusted text. */
export function escapeInternalRuntimeContextDelimiters(value: string): string {
return value
.replaceAll(INTERNAL_RUNTIME_CONTEXT_BEGIN, ESCAPED_INTERNAL_RUNTIME_CONTEXT_BEGIN)
@@ -204,6 +216,7 @@ function stripRuntimeContextPromptPreface(text: string): string {
: text;
}
/** Remove protected and legacy runtime-context blocks from text. */
export function stripInternalRuntimeContext(text: string): string {
if (!text) {
return text;
@@ -218,6 +231,7 @@ export function stripInternalRuntimeContext(text: string): string {
);
}
/** Extract protected runtime-context blocks while returning remaining visible text. */
export function extractInternalRuntimeContext(text: string): {
text: string;
runtimeContext?: string;
@@ -233,6 +247,7 @@ export function extractInternalRuntimeContext(text: string): {
};
}
/** Return true when text contains current or legacy runtime-context markers. */
export function hasInternalRuntimeContext(text: string): boolean {
if (!text) {
return false;
@@ -257,6 +272,7 @@ function isOpenClawRuntimeContextCustomMessage(message: unknown): boolean {
);
}
/** Remove all structured runtime-context custom messages. */
export function stripRuntimeContextCustomMessages<T>(messages: T[]): T[] {
if (!messages.some(isOpenClawRuntimeContextCustomMessage)) {
return messages;
+4
View File
@@ -1,3 +1,7 @@
/**
* Regression coverage for local-model lean tool filtering.
* Verifies agent scope, default flags, preserve lists, and message-tool overrides.
*/
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
+5 -4
View File
@@ -1,15 +1,16 @@
/**
* Local-model lean tool filtering.
* Removes high-latency or channel-dependent tools for local models while
* preserving explicitly required delivery tools.
*/
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope-config.js";
import type { AnyAgentTool } from "./agent-tools.types.js";
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
// Local-model lean mode removes high-latency or channel-dependent tools unless
// the caller explicitly preserves them for the current delivery path.
const LOCAL_MODEL_LEAN_DENY_TOOL_NAMES = new Set(["browser", "cron", "message"]);
// Preserve lists accept tool groups; normalize them to concrete tool names so
// filtering stays aligned with the normal tool-policy path.
function resolvePreservedLocalModelLeanToolNames(names?: Iterable<string>): Set<string> {
if (!names) {
return new Set();
+6 -2
View File
@@ -1,7 +1,11 @@
/**
* Stdio MCP launch config normalization.
* Accepts OpenClaw and upstream MCP config field names, keeping only
* command/args/env/cwd needed to spawn a stdio server.
*/
import { isMcpConfigRecord, toMcpEnvRecord, toMcpStringArray } from "./mcp-config-shared.js";
// Stdio MCP launch config normalization. Raw bundle config can use OpenClaw or
// upstream field names; this keeps only command/args/env/cwd needed to spawn.
/** Normalized stdio MCP server launch config. */
export type StdioMcpServerLaunchConfig = {
command: string;
args?: string[];
+6 -2
View File
@@ -1,15 +1,19 @@
/**
* Runtime seam for built-in model suppression.
* Lets tests and lazy catalog paths stub suppression behavior without importing
* the full suppression implementation at module load.
*/
import {
buildShouldSuppressBuiltInModel as buildShouldSuppressBuiltInModelImpl,
shouldSuppressBuiltInModel as shouldSuppressBuiltInModelImpl,
} from "./model-suppression.js";
// Runtime re-export seam for tests and lazy catalog paths that need to stub
// built-in model suppression without loading the full model suppression module.
type ShouldSuppressBuiltInModel =
typeof import("./model-suppression.js").shouldSuppressBuiltInModel;
type BuildShouldSuppressBuiltInModel =
typeof import("./model-suppression.js").buildShouldSuppressBuiltInModel;
/** Runtime-forwarded predicate for hiding bundled models. */
export function shouldSuppressBuiltInModel(
...args: Parameters<ShouldSuppressBuiltInModel>
): ReturnType<ShouldSuppressBuiltInModel> {
+4
View File
@@ -1,3 +1,7 @@
/**
* Regression coverage for model transport debug URL formatting.
* Ensures credentials, query strings, and fragments stay out of diagnostics.
*/
import { describe, expect, it } from "vitest";
import {
formatModelTransportDebugBaseUrl,
+4 -2
View File
@@ -1,5 +1,7 @@
// Debug formatting helpers for model transport endpoints. These keep logs useful
// without exposing credentials, request params, or fragments.
/**
* Debug formatting helpers for model transport endpoints.
* Keeps logs useful without exposing credentials, request params, or fragments.
*/
/** Return a sanitized URL suitable for logs and diagnostics. */
export function formatModelTransportDebugUrl(rawUrl: string): string {
try {
+4
View File
@@ -1,3 +1,7 @@
/**
* Regression coverage for depth-derived subagent capabilities.
* Verifies main/orchestrator/leaf role and control-scope decisions.
*/
import { describe, expect, it } from "vitest";
import { resolveSubagentCapabilities } from "./subagent-capabilities.js";
+9 -2
View File
@@ -1,3 +1,8 @@
/**
* Subagent capability resolution.
* Combines session-key shape, stored envelopes, spawn depth, and inherited tool
* policy to decide role, control scope, and subagent permissions.
*/
import {
resolveIntegerOption,
resolveNonNegativeIntegerOption,
@@ -18,8 +23,7 @@ import {
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import { normalizeSubagentSessionKey } from "./subagent-session-key.js";
// Subagent capability resolution for live and persisted sessions. Depth derives
// defaults, while stored envelopes can override role/scope and inherited tools.
/** Resolved role for a main session, orchestrating subagent, or leaf subagent. */
export type SubagentSessionRole = "main" | "orchestrator" | "leaf";
const SUBAGENT_SESSION_ROLES: readonly SubagentSessionRole[] = [
"main",
@@ -40,6 +44,7 @@ type SessionCapabilityEntry = {
inheritedToolDeny?: unknown;
};
/** Minimal persisted session-store shape needed to resolve subagent capabilities. */
export type SessionCapabilityStore = Record<
string,
{
@@ -126,6 +131,7 @@ function resolveSessionCapabilityEntry(params: {
return store[params.sessionKey] ?? findEntryBySessionId(store, params.sessionKey);
}
/** Resolve the session-store subset used for subagent capability lookup. */
export function resolveSubagentCapabilityStore(
sessionKey: string | undefined | null,
opts?: {
@@ -172,6 +178,7 @@ function resolveSubagentControlScopeForRole(role: SubagentSessionRole): Subagent
return role === "leaf" ? "none" : "children";
}
/** Resolve depth-derived role, scope, and spawn/control booleans. */
export function resolveSubagentCapabilities(params: { depth: number; maxSpawnDepth?: number }) {
const depth = resolveNonNegativeIntegerOption(params.depth, 0);
const role = resolveSubagentRoleForDepth(params);
+4
View File
@@ -1,3 +1,7 @@
/**
* Focused usage-normalization tests for provider token payload variants.
* Protects cache read/write and session total prompt-token calculations.
*/
import { describe, expect, it } from "vitest";
import { deriveSessionTotalTokens, hasNonzeroUsage, normalizeUsage } from "./usage.js";
+4
View File
@@ -1,3 +1,7 @@
/**
* Regression coverage for token usage normalization.
* Verifies provider usage aliases, OpenAI-compatible output, and prompt-token derivation.
*/
import { describe, expect, it } from "vitest";
import {
deriveContextPromptTokens,
+15
View File
@@ -1,5 +1,11 @@
/**
* Token usage normalization helpers.
* Converts provider-specific usage shapes into OpenClaw's normalized input,
* output, cache, reasoning, and total token accounting fields.
*/
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
/** Provider/SDK usage payload variants accepted by usage normalization. */
export type UsageLike = {
input?: number;
output?: number;
@@ -41,6 +47,7 @@ export type UsageLike = {
};
};
/** Normalized token counts used by runtime accounting. */
export type NormalizedUsage = {
input?: number;
output?: number;
@@ -50,6 +57,7 @@ export type NormalizedUsage = {
total?: number;
};
/** OpenAI chat-completions compatible usage shape. */
export type OpenAiChatCompletionsUsage = {
prompt_tokens: number;
completion_tokens: number;
@@ -58,6 +66,7 @@ export type OpenAiChatCompletionsUsage = {
completion_tokens_details?: { reasoning_tokens: number };
};
/** Assistant usage snapshot with token counts and computed cost buckets. */
export type AssistantUsageSnapshot = {
input: number;
output: number;
@@ -73,6 +82,7 @@ export type AssistantUsageSnapshot = {
};
};
/** Build a zeroed assistant usage snapshot. */
export function makeZeroUsageSnapshot(): AssistantUsageSnapshot {
return {
input: 0,
@@ -90,6 +100,7 @@ export function makeZeroUsageSnapshot(): AssistantUsageSnapshot {
};
}
/** Return true when any normalized usage bucket is positive. */
export function hasNonzeroUsage(usage?: NormalizedUsage | null): usage is NormalizedUsage {
if (!usage) {
return false;
@@ -115,6 +126,7 @@ const normalizeTokenCount = (value: unknown): number | undefined => {
return Math.min(Math.trunc(numeric), Number.MAX_SAFE_INTEGER);
};
/** Normalize provider-specific token usage fields into OpenClaw usage buckets. */
export function normalizeUsage(raw?: UsageLike | null): NormalizedUsage | undefined {
if (!raw) {
return undefined;
@@ -238,6 +250,7 @@ export function toOpenAiChatCompletionsUsage(
};
}
/** Derive prompt/context tokens from normalized input and cache buckets. */
export function derivePromptTokens(usage?: {
input?: number;
cacheRead?: number;
@@ -253,6 +266,7 @@ export function derivePromptTokens(usage?: {
return sum > 0 ? sum : undefined;
}
/** Resolve context prompt tokens from explicit override, last call, or aggregate usage. */
export function deriveContextPromptTokens(params: {
lastCallUsage?: NormalizedUsage;
promptTokens?: number;
@@ -266,6 +280,7 @@ export function deriveContextPromptTokens(params: {
return derivePromptTokens(params.lastCallUsage) ?? derivePromptTokens(params.usage);
}
/** Derive the session prompt-token snapshot stored for context display. */
export function deriveSessionTotalTokens(params: {
usage?: {
input?: number;