mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
docs: clarify inline code comments
Comment-only follow-up documenting reusable gateway, auth, proxy, device, Talk, session, and agent helper contracts.\n\nVerification: git diff --check plus targeted tests recorded in PR body.
This commit is contained in:
committed by
GitHub
parent
75e0053cf9
commit
85beee613c
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* Agent loop that works with AgentMessage throughout.
|
||||
* Transforms to Message[] only at the LLM call boundary.
|
||||
*/
|
||||
|
||||
// Keep the runtime class on the package specifier so built agent-core shares
|
||||
// constructor identity with @openclaw/llm-core; source types keep SDK d.ts bundled.
|
||||
import { EventStream as LlmEventStream } from "@openclaw/llm-core";
|
||||
@@ -26,6 +21,7 @@ import type {
|
||||
} from "./types.js";
|
||||
import { validateToolArguments } from "./validation.js";
|
||||
|
||||
/** Callback used by synchronous loop runners to publish agent lifecycle events. */
|
||||
export type AgentEventSink = (event: AgentEvent) => Promise<void> | void;
|
||||
|
||||
const EMPTY_USAGE = {
|
||||
@@ -119,6 +115,7 @@ export function agentLoopContinue(
|
||||
return stream;
|
||||
}
|
||||
|
||||
/** Run a prompt-started loop and emit events through a caller-owned sink. */
|
||||
export async function runAgentLoop(
|
||||
prompts: AgentMessage[],
|
||||
context: AgentContext,
|
||||
@@ -145,6 +142,7 @@ export async function runAgentLoop(
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
/** Continue an existing loop context and emit only newly produced messages. */
|
||||
export async function runAgentLoopContinue(
|
||||
context: AgentContext,
|
||||
config: AgentLoopConfig,
|
||||
@@ -326,10 +324,10 @@ async function runLoop(
|
||||
pendingMessages = (await config.getSteeringMessages?.()) || [];
|
||||
}
|
||||
|
||||
// Agent would stop here. Check for follow-up messages.
|
||||
const followUpMessages = (await config.getFollowUpMessages?.()) || [];
|
||||
if (followUpMessages.length > 0) {
|
||||
// Set as pending so inner loop processes them
|
||||
// Follow-up messages arrive after a turn would otherwise end; route them through the
|
||||
// same pending-message path so event ordering matches steering messages.
|
||||
pendingMessages = followUpMessages;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -100,33 +100,51 @@ function createMutableAgentState(
|
||||
|
||||
/** Options for constructing an {@link Agent}. */
|
||||
export interface AgentOptions {
|
||||
/** Initial transcript, tools, model, and prompt state. */
|
||||
initialState?: Partial<
|
||||
Omit<AgentState, "pendingToolCalls" | "isStreaming" | "streamingMessage" | "errorMessage">
|
||||
>;
|
||||
/** Convert agent-owned transcript messages into provider-facing messages. */
|
||||
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
||||
/** Optionally rewrite context before each provider request. */
|
||||
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
||||
/** Injected stream runtime used when streamFn is not supplied. */
|
||||
runtime?: AgentCoreStreamRuntimeDeps;
|
||||
/** Explicit stream implementation, preferred over runtime.streamSimple. */
|
||||
streamFn?: StreamFn;
|
||||
/** Resolve provider API keys at request time. */
|
||||
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
||||
/** Inspect the provider payload before it is sent. */
|
||||
onPayload?: SimpleStreamOptions["onPayload"];
|
||||
/** Inspect the provider response after it returns. */
|
||||
onResponse?: SimpleStreamOptions["onResponse"];
|
||||
/** Hook that may short-circuit or alter a tool call before execution. */
|
||||
beforeToolCall?: (
|
||||
context: BeforeToolCallContext,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<BeforeToolCallResult | undefined>;
|
||||
/** Hook that may alter a tool result after execution. */
|
||||
afterToolCall?: (
|
||||
context: AfterToolCallContext,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<AfterToolCallResult | undefined>;
|
||||
/** Hook that may update model, reasoning, or context after a turn. */
|
||||
prepareNextTurn?: (
|
||||
signal?: AbortSignal,
|
||||
) => Promise<AgentLoopTurnUpdate | undefined> | AgentLoopTurnUpdate | undefined;
|
||||
/** Queue drain mode for steering messages injected before the next assistant response. */
|
||||
steeringMode?: QueueMode;
|
||||
/** Queue drain mode for follow-up messages injected after the agent would otherwise stop. */
|
||||
followUpMode?: QueueMode;
|
||||
/** Session identifier forwarded to cache-aware providers. */
|
||||
sessionId?: string;
|
||||
/** Optional per-thinking-level token budgets forwarded to providers. */
|
||||
thinkingBudgets?: ThinkingBudgets;
|
||||
/** Preferred provider transport. */
|
||||
transport?: Transport;
|
||||
/** Optional cap for provider-requested retry delays. */
|
||||
maxRetryDelayMs?: number;
|
||||
/** Default strategy for executing multiple tool calls in one assistant message. */
|
||||
toolExecution?: ToolExecutionMode;
|
||||
}
|
||||
|
||||
@@ -153,6 +171,7 @@ class PendingMessageQueue {
|
||||
return drained;
|
||||
}
|
||||
|
||||
// one-at-a-time preserves later queued messages for subsequent loop turns.
|
||||
const first = this.messages[0];
|
||||
if (!first) {
|
||||
return [];
|
||||
|
||||
@@ -51,11 +51,15 @@ export interface CollectEntriesResult {
|
||||
commonAncestorId: string | null;
|
||||
}
|
||||
|
||||
/** Minimal tree entry shape needed to compare two session branches. */
|
||||
export interface BranchPathEntry {
|
||||
/** Stable entry id. */
|
||||
id: string;
|
||||
/** Parent entry id, or null for the session root. */
|
||||
parentId: string | null;
|
||||
}
|
||||
|
||||
/** Branch entries selected after comparing old and target paths. */
|
||||
export interface CollectBranchPathEntriesResult<TEntry extends BranchPathEntry> {
|
||||
/** Entries to summarize in chronological order. */
|
||||
entries: TEntry[];
|
||||
@@ -106,7 +110,7 @@ export function collectEntriesForBranchSummaryFromBranches<TEntry extends Branch
|
||||
return { entries: oldBranch.slice(firstSummarizedIndex), commonAncestorId };
|
||||
}
|
||||
|
||||
/** Collect entries that should be summarized before navigating to a different session tree entry. */
|
||||
/** Collect concrete session entries to summarize before moving from one leaf to another. */
|
||||
export async function collectEntriesForBranchSummary(
|
||||
session: Session,
|
||||
oldLeafId: string | null,
|
||||
@@ -191,6 +195,8 @@ export function prepareBranchEntries(
|
||||
|
||||
const tokens = estimateTokens(message);
|
||||
if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
|
||||
// Prefer already-compressed summaries when the budget is almost filled; they
|
||||
// preserve older branch context better than dropping the whole prefix.
|
||||
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
||||
if (totalTokens < tokenBudget * 0.9) {
|
||||
messages.unshift(message);
|
||||
|
||||
@@ -35,6 +35,7 @@ function resolvePath(cwd: string, path: string): string {
|
||||
return isAbsolute(path) ? path : resolve(cwd, path);
|
||||
}
|
||||
|
||||
/** Convert user-facing timeout seconds into a positive, timer-safe millisecond delay. */
|
||||
export function resolveExecTimeoutMs(timeoutSeconds: unknown): number | undefined {
|
||||
if (
|
||||
typeof timeoutSeconds !== "number" ||
|
||||
@@ -241,6 +242,7 @@ function getShellEnv(
|
||||
};
|
||||
}
|
||||
|
||||
/** Node-backed execution environment for agent harness filesystem and shell operations. */
|
||||
export class NodeExecutionEnv implements ExecutionEnv {
|
||||
cwd: string;
|
||||
private shellPath?: string;
|
||||
|
||||
@@ -12,6 +12,7 @@ interface FileInfoDiagnostics {
|
||||
push(diagnostic: FileInfoDiagnostic): unknown;
|
||||
}
|
||||
|
||||
/** Parse optional YAML frontmatter and return the normalized Markdown body. */
|
||||
export function parseFrontmatter(
|
||||
content: string,
|
||||
): Result<{ frontmatter: Record<string, unknown>; body: string }, Error> {
|
||||
@@ -35,6 +36,7 @@ export function parseFrontmatter(
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve symlink or unknown file info into the concrete loadable file kind. */
|
||||
export async function resolveFileInfoKind(
|
||||
env: ExecutionEnv,
|
||||
info: FileInfo,
|
||||
@@ -72,22 +74,26 @@ export async function resolveFileInfoKind(
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Join harness environment paths without requiring Node path semantics. */
|
||||
export function joinEnvPath(base: string, child: string): string {
|
||||
return `${base.replace(/\/+$/, "")}/${child.replace(/^\/+/, "")}`;
|
||||
}
|
||||
|
||||
/** Return the parent path for slash-separated harness environment paths. */
|
||||
export function dirnameEnvPath(path: string): string {
|
||||
const normalized = path.replace(/\/+$/, "");
|
||||
const slashIndex = normalized.lastIndexOf("/");
|
||||
return slashIndex <= 0 ? "/" : normalized.slice(0, slashIndex);
|
||||
}
|
||||
|
||||
/** Return the leaf name for slash-separated harness environment paths. */
|
||||
export function basenameEnvPath(path: string): string {
|
||||
const normalized = path.replace(/\/+$/, "");
|
||||
const slashIndex = normalized.lastIndexOf("/");
|
||||
return slashIndex === -1 ? normalized : normalized.slice(slashIndex + 1);
|
||||
}
|
||||
|
||||
/** Return a root-relative path when possible, otherwise a display-safe non-absolute path. */
|
||||
export function relativeEnvPath(root: string, path: string): string {
|
||||
const normalizedRoot = root.replace(/\/+$/, "");
|
||||
const normalizedPath = path.replace(/\/+$/, "");
|
||||
|
||||
@@ -15,6 +15,7 @@ export type {
|
||||
CustomMessage,
|
||||
} from "../types.js";
|
||||
|
||||
/** Harness-only transcript entries that can be normalized into LLM messages. */
|
||||
export type HarnessMessage =
|
||||
| AgentMessage
|
||||
| BashExecutionMessage
|
||||
@@ -52,6 +53,7 @@ export const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch tha
|
||||
|
||||
export const BRANCH_SUMMARY_SUFFIX = `</summary>`;
|
||||
|
||||
/** Render a shell execution record as user-visible context text for the model. */
|
||||
export function bashExecutionToText(msg: BashExecutionMessage): string {
|
||||
let text = `Ran \`${msg.command}\`\n`;
|
||||
if (msg.output) {
|
||||
@@ -70,6 +72,7 @@ export function bashExecutionToText(msg: BashExecutionMessage): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
/** Build a persisted branch summary message from the repository timestamp string. */
|
||||
export function createBranchSummaryMessage(
|
||||
summary: string,
|
||||
fromId: string,
|
||||
@@ -83,6 +86,7 @@ export function createBranchSummaryMessage(
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a persisted compaction summary message from the repository timestamp string. */
|
||||
export function createCompactionSummaryMessage(
|
||||
summary: string,
|
||||
tokensBefore: number,
|
||||
@@ -96,6 +100,7 @@ export function createCompactionSummaryMessage(
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a custom transcript message that can be shown and replayed into context. */
|
||||
export function createCustomMessage(
|
||||
customType: string,
|
||||
content: string | (TextContent | ImageContent)[],
|
||||
@@ -113,6 +118,7 @@ export function createCustomMessage(
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert harness transcript messages into the LLM-facing message sequence. */
|
||||
export function convertToLlm(messages: AgentMessage[]): Message[] {
|
||||
return messages
|
||||
.map((m): Message | undefined => {
|
||||
|
||||
@@ -33,7 +33,12 @@ function parseSafeNonNegativeInteger(raw: string): number | undefined {
|
||||
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
/** Substitute prompt template placeholders (`$1`, `$@`, `$ARGUMENTS`, `${@:N}`, `${@:N:L}`) with command arguments. */
|
||||
/**
|
||||
* Substitute prompt template placeholders (`$1`, `$@`, `$ARGUMENTS`, `${@:N}`, `${@:N:L}`) with command arguments.
|
||||
*
|
||||
* Unsafe integer placeholders resolve to empty text instead of throwing, so malformed templates cannot abort prompt
|
||||
* loading or invocation.
|
||||
*/
|
||||
export function substituteArgs(content: string, args: string[]): string {
|
||||
let result = content;
|
||||
result = result.replace(/\$(\d+)/g, (_, num: string) => {
|
||||
@@ -50,6 +55,8 @@ export function substituteArgs(content: string, args: string[]): string {
|
||||
if (parsedStart === undefined) {
|
||||
return "";
|
||||
}
|
||||
// Keep shell-style `${@:0:...}` compatibility: start 0 includes `$0` in shell, but
|
||||
// prompt templates have no command name, so it maps to the first provided argument.
|
||||
let start = parsedStart - 1;
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
|
||||
@@ -36,6 +36,7 @@ function encodeCwd(cwd: string): string {
|
||||
return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
||||
}
|
||||
|
||||
/** Repository for JSONL sessions grouped by working directory. */
|
||||
export class JsonlSessionRepo implements JsonlSessionRepoApi {
|
||||
private readonly fs: JsonlSessionRepoFileSystem;
|
||||
private readonly sessionsRootInput: string;
|
||||
@@ -130,6 +131,8 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi {
|
||||
sessions.push(await loadJsonlSessionMetadata(this.fs, file.path));
|
||||
} catch (error) {
|
||||
const cause = toError(error);
|
||||
// Listing is best-effort across a sessions directory; corrupt session
|
||||
// headers are skipped, while filesystem and unexpected errors still fail.
|
||||
if (!(cause instanceof SessionError) || cause.code !== "invalid_session") {
|
||||
throw cause;
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ function headerToSessionMetadata(header: SessionHeader, path: string): JsonlSess
|
||||
};
|
||||
}
|
||||
|
||||
/** Read only the JSONL session header and convert it to session metadata. */
|
||||
export async function loadJsonlSessionMetadata(
|
||||
fs: JsonlSessionStorageFileSystem,
|
||||
filePath: string,
|
||||
@@ -168,6 +169,7 @@ async function loadJsonlStorage(
|
||||
return { header, entries, leafId };
|
||||
}
|
||||
|
||||
/** Append-only JSONL-backed storage for one session tree. */
|
||||
export class JsonlSessionStorage extends BaseSessionStorage<JsonlSessionMetadata> {
|
||||
private readonly fs: JsonlSessionStorageFileSystem;
|
||||
private readonly filePath: string;
|
||||
@@ -192,6 +194,7 @@ export class JsonlSessionStorage extends BaseSessionStorage<JsonlSessionMetadata
|
||||
return new JsonlSessionStorage(fs, filePath, loaded.header, loaded.entries, loaded.leafId);
|
||||
}
|
||||
|
||||
/** Create a new JSONL file with a session header and no entries. */
|
||||
static async create(
|
||||
fs: JsonlSessionStorageFileSystem,
|
||||
filePath: string,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { SessionMetadata, SessionTreeEntry } from "../types.js";
|
||||
import { BaseSessionStorage } from "./storage-base.js";
|
||||
import { uuidv7 } from "./uuid.js";
|
||||
|
||||
/** Volatile session storage used by tests and in-process harness callers. */
|
||||
export class InMemorySessionStorage<
|
||||
TMetadata extends SessionMetadata = SessionMetadata,
|
||||
> extends BaseSessionStorage<TMetadata> {
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
} from "../types.js";
|
||||
import { SessionError } from "../types.js";
|
||||
|
||||
/** Build model context from the active session branch and its latest state markers. */
|
||||
export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionContext {
|
||||
let thinkingLevel = "off";
|
||||
let model: { provider: string; modelId: string } | null = null;
|
||||
@@ -76,6 +77,8 @@ export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionCon
|
||||
const compactionIdx = pathEntries.findIndex(
|
||||
(e) => e.type === "compaction" && e.id === compaction.id,
|
||||
);
|
||||
// Replay only the compacted entry's retained tail plus newer branch entries; older
|
||||
// transcript content is represented by the synthetic compaction summary above.
|
||||
let foundFirstKept = false;
|
||||
for (let i = 0; i < compactionIdx; i++) {
|
||||
const entry = pathEntries[i];
|
||||
@@ -98,6 +101,7 @@ export function buildSessionContext(pathEntries: SessionTreeEntry[]): SessionCon
|
||||
return { messages, thinkingLevel, model };
|
||||
}
|
||||
|
||||
/** High-level session API backed by pluggable tree storage. */
|
||||
export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
|
||||
private storage: SessionStorage<TMetadata>;
|
||||
|
||||
@@ -199,6 +203,7 @@ export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
|
||||
} satisfies CompactionEntry);
|
||||
}
|
||||
|
||||
/** Append a non-LLM transcript marker for harness-specific state. */
|
||||
async appendCustomEntry(customType: string, data?: unknown): Promise<string> {
|
||||
return this.appendTypedEntry({
|
||||
type: "custom",
|
||||
@@ -210,6 +215,7 @@ export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
|
||||
} satisfies CustomEntry);
|
||||
}
|
||||
|
||||
/** Append harness-specific content that can also be replayed into model context. */
|
||||
async appendCustomMessageEntry(
|
||||
customType: string,
|
||||
content: string | (TextContent | ImageContent)[],
|
||||
@@ -228,6 +234,7 @@ export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
|
||||
} satisfies CustomMessageEntry);
|
||||
}
|
||||
|
||||
/** Record or clear the display label for an existing session entry. */
|
||||
async appendLabel(targetId: string, label: string | undefined): Promise<string> {
|
||||
if (!(await this.storage.getEntry(targetId))) {
|
||||
throw new SessionError("not_found", `Entry ${targetId} not found`);
|
||||
@@ -252,6 +259,7 @@ export class Session<TMetadata extends SessionMetadata = SessionMetadata> {
|
||||
} satisfies SessionInfoEntry);
|
||||
}
|
||||
|
||||
/** Move the visible branch leaf and optionally attach a summary of the abandoned branch. */
|
||||
async moveTo(
|
||||
entryId: string | null,
|
||||
summary?: { summary: string; details?: unknown; fromHook?: boolean },
|
||||
|
||||
@@ -37,6 +37,7 @@ function generateEntryId(byId: { has(id: string): boolean }): string {
|
||||
return uuidv7();
|
||||
}
|
||||
|
||||
/** Return the effective branch leaf after applying a session tree entry. */
|
||||
export function leafIdAfterEntry(entry: SessionTreeEntry): string | null {
|
||||
return entry.type === "leaf" ? entry.targetId : entry.id;
|
||||
}
|
||||
@@ -102,6 +103,8 @@ export abstract class BaseSessionStorage<
|
||||
}
|
||||
|
||||
protected recordEntry(entry: SessionTreeEntry): void {
|
||||
// Leaf and label entries are append-only state changes; keep derived indexes
|
||||
// synchronized here so memory and JSONL storage expose identical behavior.
|
||||
this.entries.push(entry);
|
||||
this.byId.set(entry.id, entry);
|
||||
updateLabelCache(this.labelsById, entry);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Skill } from "./types.js";
|
||||
|
||||
/** Format model-visible skill metadata for inclusion in the harness system prompt. */
|
||||
export function formatSkillsForSystemPrompt(skills: Skill[]): string {
|
||||
// Hidden skills can still be invoked directly by host code, but should not be
|
||||
// advertised to the model for autonomous selection.
|
||||
const visibleSkills = skills.filter((skill) => !skill.disableModelInvocation);
|
||||
if (visibleSkills.length === 0) {
|
||||
return "";
|
||||
|
||||
@@ -361,29 +361,38 @@ export interface Shell {
|
||||
/** Filesystem and process execution environment used by the harness. */
|
||||
export interface ExecutionEnv extends FileSystem, Shell {}
|
||||
|
||||
/** Base fields shared by append-only session tree entries. */
|
||||
export interface SessionTreeEntryBase {
|
||||
/** Entry discriminator used for JSONL persistence and typed narrowing. */
|
||||
type: string;
|
||||
/** Stable entry id unique within a session file. */
|
||||
id: string;
|
||||
/** Parent entry id, or null for a root entry. */
|
||||
parentId: string | null;
|
||||
/** ISO timestamp string used for persistence and sorting. */
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/** Persisted transcript message entry. */
|
||||
export interface MessageEntry extends SessionTreeEntryBase {
|
||||
type: "message";
|
||||
message: AgentMessage;
|
||||
}
|
||||
|
||||
/** Persisted thinking-level selection marker. */
|
||||
export interface ThinkingLevelChangeEntry extends SessionTreeEntryBase {
|
||||
type: "thinking_level_change";
|
||||
thinkingLevel: string;
|
||||
}
|
||||
|
||||
/** Persisted model selection marker. */
|
||||
export interface ModelChangeEntry extends SessionTreeEntryBase {
|
||||
type: "model_change";
|
||||
provider: string;
|
||||
modelId: string;
|
||||
}
|
||||
|
||||
/** Persisted summary that replaces older transcript history in context. */
|
||||
export interface CompactionEntry<T = unknown> extends SessionTreeEntryBase {
|
||||
type: "compaction";
|
||||
summary: string;
|
||||
@@ -393,6 +402,7 @@ export interface CompactionEntry<T = unknown> extends SessionTreeEntryBase {
|
||||
fromHook?: boolean;
|
||||
}
|
||||
|
||||
/** Persisted summary of an abandoned branch when navigating the session tree. */
|
||||
export interface BranchSummaryEntry<T = unknown> extends SessionTreeEntryBase {
|
||||
type: "branch_summary";
|
||||
fromId: string;
|
||||
@@ -401,12 +411,14 @@ export interface BranchSummaryEntry<T = unknown> extends SessionTreeEntryBase {
|
||||
fromHook?: boolean;
|
||||
}
|
||||
|
||||
/** Persisted harness/application marker that is not replayed into model context. */
|
||||
export interface CustomEntry<T = unknown> extends SessionTreeEntryBase {
|
||||
type: "custom";
|
||||
customType: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/** Persisted harness/application message that can be replayed into model context. */
|
||||
export interface CustomMessageEntry<T = unknown> extends SessionTreeEntryBase {
|
||||
type: "custom_message";
|
||||
customType: string;
|
||||
@@ -415,22 +427,27 @@ export interface CustomMessageEntry<T = unknown> extends SessionTreeEntryBase {
|
||||
display: boolean;
|
||||
}
|
||||
|
||||
/** Append-only label update for another session entry. */
|
||||
export interface LabelEntry extends SessionTreeEntryBase {
|
||||
type: "label";
|
||||
targetId: string;
|
||||
label: string | undefined;
|
||||
}
|
||||
|
||||
/** Persisted session metadata marker. */
|
||||
export interface SessionInfoEntry extends SessionTreeEntryBase {
|
||||
type: "session_info"; // legacy name, kept for backwards compatibility
|
||||
// The persisted discriminator predates the public "session name" wording.
|
||||
type: "session_info";
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/** Append-only marker that changes the active visible leaf. */
|
||||
export interface LeafEntry extends SessionTreeEntryBase {
|
||||
type: "leaf";
|
||||
targetId: string | null;
|
||||
}
|
||||
|
||||
/** All persisted session tree entry variants. */
|
||||
export type SessionTreeEntry =
|
||||
| MessageEntry
|
||||
| ThinkingLevelChangeEntry
|
||||
@@ -679,28 +696,36 @@ export type AgentHarnessEvent<
|
||||
TPromptTemplate extends PromptTemplate = PromptTemplate,
|
||||
> = AgentEvent | AgentHarnessOwnEvent<TSkill, TPromptTemplate>;
|
||||
|
||||
/** Hook result for mutating the initial prompt run before the agent starts. */
|
||||
export interface BeforeAgentStartResult {
|
||||
/** Replacement messages for the prompt run. */
|
||||
messages?: AgentMessage[];
|
||||
/** Replacement system prompt for the prompt run. */
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
/** Hook result for replacing the full context message list before provider conversion. */
|
||||
export interface ContextResult {
|
||||
messages: AgentMessage[];
|
||||
}
|
||||
|
||||
/** Hook result for patching provider request options before payload construction. */
|
||||
export interface BeforeProviderRequestResult {
|
||||
streamOptions?: AgentHarnessStreamOptionsPatch;
|
||||
}
|
||||
|
||||
/** Hook result for replacing the provider payload after construction. */
|
||||
export interface BeforeProviderPayloadResult {
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
/** Hook result for blocking a tool call before execution. */
|
||||
export interface ToolCallResult {
|
||||
block?: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** Hook patch for a completed tool result before it is persisted/emitted. */
|
||||
export interface ToolResultPatch {
|
||||
content?: Array<TextContent | ImageContent>;
|
||||
details?: unknown;
|
||||
@@ -708,11 +733,13 @@ export interface ToolResultPatch {
|
||||
terminate?: boolean;
|
||||
}
|
||||
|
||||
/** Hook result for cancelling or replacing a planned compaction. */
|
||||
export interface SessionBeforeCompactResult {
|
||||
cancel?: boolean;
|
||||
compaction?: CompactResult;
|
||||
}
|
||||
|
||||
/** Hook result for cancelling, labeling, or supplying branch-summary behavior before tree navigation. */
|
||||
export interface SessionBeforeTreeResult {
|
||||
cancel?: boolean;
|
||||
summary?: { summary: string; details?: unknown };
|
||||
@@ -721,6 +748,7 @@ export interface SessionBeforeTreeResult {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** Typed return values expected from AgentHarness hook handlers by event type. */
|
||||
export type AgentHarnessEventResultMap = {
|
||||
before_agent_start: BeforeAgentStartResult | undefined;
|
||||
context: ContextResult | undefined;
|
||||
@@ -742,15 +770,18 @@ export type AgentHarnessEventResultMap = {
|
||||
settled: undefined;
|
||||
};
|
||||
|
||||
/** Options for a prompt submitted through AgentHarness. */
|
||||
export interface AgentHarnessPromptOptions {
|
||||
images?: ImageContent[];
|
||||
}
|
||||
|
||||
/** Queued messages removed by an abort operation. */
|
||||
export interface AbortResult {
|
||||
clearedSteer: AgentMessage[];
|
||||
clearedFollowUp: AgentMessage[];
|
||||
}
|
||||
|
||||
/** Compaction data supplied by hooks or returned from compaction preparation. */
|
||||
export interface CompactResult {
|
||||
summary: string;
|
||||
firstKeptEntryId: string;
|
||||
@@ -758,18 +789,21 @@ export interface CompactResult {
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
/** Result of moving the active session-tree leaf. */
|
||||
export interface NavigateTreeResult {
|
||||
cancelled: boolean;
|
||||
editorText?: string;
|
||||
summaryEntry?: BranchSummaryEntry;
|
||||
}
|
||||
|
||||
/** Settings that control automatic context compaction. */
|
||||
export interface CompactionSettings {
|
||||
enabled: boolean;
|
||||
reserveTokens: number;
|
||||
keepRecentTokens: number;
|
||||
}
|
||||
|
||||
/** Prepared compaction inputs exposed to hooks before a summary is generated. */
|
||||
export interface CompactionPreparation {
|
||||
firstKeptEntryId: string;
|
||||
messagesToSummarize: AgentMessage[];
|
||||
@@ -781,12 +815,14 @@ export interface CompactionPreparation {
|
||||
settings: CompactionSettings;
|
||||
}
|
||||
|
||||
/** File operations accumulated from summarized transcript ranges. */
|
||||
export interface FileOperations {
|
||||
read: Set<string>;
|
||||
written: Set<string>;
|
||||
edited: Set<string>;
|
||||
}
|
||||
|
||||
/** Prepared branch navigation inputs exposed to hooks before a summary is generated. */
|
||||
export interface TreePreparation {
|
||||
targetId: string;
|
||||
oldLeafId: string | null;
|
||||
@@ -798,6 +834,7 @@ export interface TreePreparation {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/** Options for generating a branch summary. */
|
||||
export interface GenerateBranchSummaryOptions {
|
||||
model: Model;
|
||||
apiKey: string;
|
||||
@@ -810,12 +847,14 @@ export interface GenerateBranchSummaryOptions {
|
||||
reserveTokens?: number;
|
||||
}
|
||||
|
||||
/** Generated branch summary text and file-operation metadata. */
|
||||
export interface BranchSummaryResult {
|
||||
summary: string;
|
||||
readFiles: string[];
|
||||
modifiedFiles: string[];
|
||||
}
|
||||
|
||||
/** Construction options for AgentHarness. */
|
||||
export interface AgentHarnessOptions<
|
||||
TSkill extends Skill = Skill,
|
||||
TPromptTemplate extends PromptTemplate = PromptTemplate,
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
/**
|
||||
* Shared truncation utilities for tool outputs.
|
||||
*
|
||||
* Truncation is based on two independent limits - whichever is hit first wins:
|
||||
* - Line limit (default: 2000 lines)
|
||||
* - Byte limit (default: 50KB)
|
||||
*
|
||||
* Never returns partial lines (except bash tail truncation edge case).
|
||||
*/
|
||||
|
||||
export const DEFAULT_MAX_LINES = 2000;
|
||||
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
||||
export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
|
||||
|
||||
/** Result metadata for content truncated by line count, byte count, or both. */
|
||||
export interface TruncationResult {
|
||||
/** The truncated content */
|
||||
content: string;
|
||||
@@ -37,6 +28,7 @@ export interface TruncationResult {
|
||||
maxBytes: number;
|
||||
}
|
||||
|
||||
/** Byte and line ceilings used by the truncation helpers. */
|
||||
export interface TruncationOptions {
|
||||
/** Maximum number of lines (default: 2000) */
|
||||
maxLines?: number;
|
||||
@@ -134,7 +126,7 @@ function replaceUnpairedSurrogates(content: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes as human-readable size.
|
||||
* Format byte counts for compact tool-output diagnostics.
|
||||
*/
|
||||
export function formatSize(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
@@ -190,11 +182,10 @@ function buildTruncationResult(
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate content from the head (keep first N lines/bytes).
|
||||
* Suitable for file reads where you want to see the beginning.
|
||||
* Keep the beginning of content while respecting independent line and byte ceilings.
|
||||
*
|
||||
* Never returns partial lines. If first line exceeds byte limit,
|
||||
* returns empty content with firstLineExceedsLimit=true.
|
||||
* Head truncation preserves complete lines; a first line that exceeds the byte
|
||||
* ceiling produces empty output and sets firstLineExceedsLimit.
|
||||
*/
|
||||
export function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {
|
||||
const input = resolveTruncationInput(content, options);
|
||||
@@ -257,10 +248,10 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate content from the tail (keep last N lines/bytes).
|
||||
* Suitable for bash output where you want to see the end (errors, final results).
|
||||
* Keep the end of content while respecting independent line and byte ceilings.
|
||||
*
|
||||
* May return partial first line if the last line of original content exceeds byte limit.
|
||||
* Tail truncation preserves recent output for command errors and may keep a
|
||||
* partial first line when one final line alone exceeds the byte ceiling.
|
||||
*/
|
||||
export function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {
|
||||
const input = resolveTruncationInput(content, options);
|
||||
@@ -366,8 +357,7 @@ function truncateStringToBytesFromEnd(str: string, maxBytes: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a single line to max characters, adding [truncated] suffix.
|
||||
* Used for grep match lines.
|
||||
* Trim a single display line and mark it with the grep-style truncation suffix.
|
||||
*/
|
||||
export function truncateLine(
|
||||
line: string,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { CompleteSimpleFn, StreamFn } from "../../llm-core/src/index.js";
|
||||
|
||||
/** Runtime functions injected by host packages so agent-core stays provider-agnostic. */
|
||||
export interface AgentCoreRuntimeDeps {
|
||||
/** Streaming completion implementation used for normal agent turns. */
|
||||
streamSimple: StreamFn;
|
||||
/** Non-streaming completion implementation used by summarization helpers. */
|
||||
completeSimple: CompleteSimpleFn;
|
||||
}
|
||||
|
||||
/** Runtime dependency subset required by streaming agent loops. */
|
||||
export type AgentCoreStreamRuntimeDeps = Pick<AgentCoreRuntimeDeps, "streamSimple">;
|
||||
/** Runtime dependency subset required by summarization helpers. */
|
||||
export type AgentCoreCompletionRuntimeDeps = Pick<AgentCoreRuntimeDeps, "completeSimple">;
|
||||
|
||||
function missingRuntimeDep(name: keyof AgentCoreRuntimeDeps): Error {
|
||||
@@ -14,6 +19,7 @@ function missingRuntimeDep(name: keyof AgentCoreRuntimeDeps): Error {
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolve the stream function, preferring an explicit override over injected runtime deps. */
|
||||
export function resolveAgentCoreStreamFn(
|
||||
runtime: AgentCoreStreamRuntimeDeps | undefined,
|
||||
streamFn?: StreamFn,
|
||||
@@ -27,6 +33,7 @@ export function resolveAgentCoreStreamFn(
|
||||
throw missingRuntimeDep("streamSimple");
|
||||
}
|
||||
|
||||
/** Resolve the completion function used by non-streaming helper flows. */
|
||||
export function resolveAgentCoreCompleteFn(
|
||||
runtime: AgentCoreCompletionRuntimeDeps | undefined,
|
||||
): CompleteSimpleFn {
|
||||
|
||||
@@ -288,40 +288,66 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
||||
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
|
||||
export interface BashExecutionMessage {
|
||||
/** Harness role for shell command transcripts. */
|
||||
role: "bashExecution";
|
||||
/** Command line that was executed. */
|
||||
command: string;
|
||||
/** Captured command output, usually already truncated for context. */
|
||||
output: string;
|
||||
/** Process exit code when the command reached process exit. */
|
||||
exitCode: number | undefined;
|
||||
/** True when the command was interrupted before normal completion. */
|
||||
cancelled: boolean;
|
||||
/** True when output was shortened for transcript/context storage. */
|
||||
truncated: boolean;
|
||||
/** Optional path containing the complete output when truncation occurred. */
|
||||
fullOutputPath?: string;
|
||||
/** Millisecond timestamp for transcript ordering. */
|
||||
timestamp: number;
|
||||
/** Exclude this command transcript from model context while keeping it in session history. */
|
||||
excludeFromContext?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomMessage<T = unknown> {
|
||||
/** Harness role for application-defined transcript content. */
|
||||
role: "custom";
|
||||
/** Application-defined discriminator for rendering or handling this message. */
|
||||
customType: string;
|
||||
/** Content replayed into model context when this message is included. */
|
||||
content: string | (TextContent | ImageContent)[];
|
||||
/** Whether UI surfaces should display this message. */
|
||||
display: boolean;
|
||||
/** Optional application-specific metadata. */
|
||||
details?: T;
|
||||
/** Millisecond timestamp for transcript ordering. */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface BranchSummaryMessage {
|
||||
/** Harness role for summaries produced when returning from another branch. */
|
||||
role: "branchSummary";
|
||||
/** Summary text inserted back into model context. */
|
||||
summary: string;
|
||||
/** Entry id of the branch root or source leaf being summarized. */
|
||||
fromId: string;
|
||||
/** Millisecond timestamp for transcript ordering. */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface CompactionSummaryMessage {
|
||||
/** Harness role for summaries that replace compacted transcript history. */
|
||||
role: "compactionSummary";
|
||||
/** Summary text inserted back into model context. */
|
||||
summary: string;
|
||||
/** Estimated context tokens before compaction. */
|
||||
tokensBefore: number;
|
||||
/** Timestamp may be numeric in memory or string when loaded from older persisted rows. */
|
||||
timestamp: number | string;
|
||||
/** Optional estimated context tokens after compaction. */
|
||||
tokensAfter?: number;
|
||||
/** Optional first retained entry id from the compaction range. */
|
||||
firstKeptEntryId?: string;
|
||||
/** Optional implementation-specific compaction metadata. */
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolveFiniteTimeoutDelayMs } from "./timeouts.js";
|
||||
|
||||
/** Readiness probe outcome with timing data for diagnosing event-loop stalls. */
|
||||
export type EventLoopReadyResult = {
|
||||
ready: boolean;
|
||||
elapsedMs: number;
|
||||
@@ -8,6 +9,7 @@ export type EventLoopReadyResult = {
|
||||
aborted: boolean;
|
||||
};
|
||||
|
||||
/** Controls how aggressively the client waits for low-drift timer checks before starting IO. */
|
||||
export type EventLoopReadyOptions = {
|
||||
maxWaitMs?: number;
|
||||
intervalMs?: number;
|
||||
@@ -25,6 +27,7 @@ function resolvePositiveInteger(value: number | undefined, fallback: number): nu
|
||||
return Number.isFinite(value) && value !== undefined ? Math.max(1, Math.floor(value)) : fallback;
|
||||
}
|
||||
|
||||
/** Waits until timer drift stays low for consecutive checks, or aborts/times out. */
|
||||
export async function waitForEventLoopReady(
|
||||
options: EventLoopReadyOptions = {},
|
||||
): Promise<EventLoopReadyResult> {
|
||||
|
||||
@@ -10,10 +10,12 @@ export type GatewayClientStartable = {
|
||||
start(): void;
|
||||
};
|
||||
|
||||
/** Injectable readiness waiter used by tests and alternate event-loop probes. */
|
||||
export type EventLoopReadyWaiter = (
|
||||
options?: EventLoopReadyOptions,
|
||||
) => Promise<EventLoopReadyResult>;
|
||||
|
||||
/** Timeout and abort controls for delaying client start until the loop can process IO. */
|
||||
export type GatewayClientStartReadinessOptions = {
|
||||
timeoutMs?: number;
|
||||
clientOptions?: Pick<
|
||||
@@ -43,6 +45,7 @@ function resolveGatewayClientStartReadinessTimeoutMs(
|
||||
});
|
||||
}
|
||||
|
||||
/** Starts a gateway client only after the supplied readiness probe succeeds. */
|
||||
export async function startGatewayClientWithReadinessWait(
|
||||
waitForReady: EventLoopReadyWaiter,
|
||||
client: GatewayClientStartable,
|
||||
@@ -58,6 +61,7 @@ export async function startGatewayClientWithReadinessWait(
|
||||
return readiness;
|
||||
}
|
||||
|
||||
/** Starts a gateway client after the default event-loop readiness probe succeeds. */
|
||||
export async function startGatewayClientWhenEventLoopReady(
|
||||
client: GatewayClientStartable,
|
||||
options: GatewayClientStartReadinessOptions = {},
|
||||
|
||||
@@ -7,11 +7,16 @@ function parseStrictPositiveInteger(value: string): number | undefined {
|
||||
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
/** Maximum delay Node timers can represent without overflow warnings. */
|
||||
export const MAX_SAFE_TIMEOUT_DELAY_MS = 2_147_483_647;
|
||||
/** Default server-side window for gateway preauth handshakes. */
|
||||
export const DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 15_000;
|
||||
/** Minimum client watchdog delay for connect challenge setup. */
|
||||
export const MIN_CONNECT_CHALLENGE_TIMEOUT_MS = 250;
|
||||
/** Default maximum client watchdog delay, aligned with the preauth server timeout. */
|
||||
export const MAX_CONNECT_CHALLENGE_TIMEOUT_MS = DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS;
|
||||
|
||||
/** Clamps arbitrary timer delays to Node's safe range and an optional floor. */
|
||||
export function resolveSafeTimeoutDelayMs(delayMs: number, opts?: { minMs?: number }): number {
|
||||
const rawMinMs = opts?.minMs ?? 1;
|
||||
const minMs = Math.min(
|
||||
@@ -22,6 +27,7 @@ export function resolveSafeTimeoutDelayMs(delayMs: number, opts?: { minMs?: numb
|
||||
return Math.min(MAX_SAFE_TIMEOUT_DELAY_MS, Math.max(minMs, candidateMs));
|
||||
}
|
||||
|
||||
/** Adds grace time while preserving safe timer bounds if inputs overflow or are invalid. */
|
||||
export function addSafeTimeoutDelayGraceMs(
|
||||
delayMs: number,
|
||||
graceMs: number,
|
||||
@@ -37,6 +43,7 @@ export function addSafeTimeoutDelayGraceMs(
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolves optional timeout values through a fallback and safe timer clamp. */
|
||||
export function resolveFiniteTimeoutDelayMs(
|
||||
delayMs: number | null | undefined,
|
||||
fallbackMs: number,
|
||||
@@ -47,6 +54,7 @@ export function resolveFiniteTimeoutDelayMs(
|
||||
return resolveSafeTimeoutDelayMs(candidateMs, opts);
|
||||
}
|
||||
|
||||
/** Clamps connect challenge watchdog timeouts to the gateway-supported range. */
|
||||
export function clampConnectChallengeTimeoutMs(
|
||||
timeoutMs: number,
|
||||
maxTimeoutMs = MAX_CONNECT_CHALLENGE_TIMEOUT_MS,
|
||||
@@ -57,6 +65,7 @@ export function clampConnectChallengeTimeoutMs(
|
||||
);
|
||||
}
|
||||
|
||||
/** Reads the connect challenge watchdog override from the process environment. */
|
||||
export function getConnectChallengeTimeoutMsFromEnv(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): number | undefined {
|
||||
@@ -76,6 +85,7 @@ function normalizePositiveTimeoutMs(timeoutMs: unknown): number | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Resolves the client watchdog timeout using explicit, env, then preauth defaults. */
|
||||
export function resolveConnectChallengeTimeoutMs(
|
||||
timeoutMs?: number | null,
|
||||
params?: {
|
||||
@@ -100,6 +110,7 @@ export function resolveConnectChallengeTimeoutMs(
|
||||
return clampConnectChallengeTimeoutMs(configuredPreauthTimeoutMs, maxTimeoutMs);
|
||||
}
|
||||
|
||||
/** Reads the preauth handshake timeout override from environment variables. */
|
||||
export function getPreauthHandshakeTimeoutMsFromEnv(env: NodeJS.ProcessEnv = process.env): number {
|
||||
const configuredTimeout =
|
||||
env.OPENCLAW_HANDSHAKE_TIMEOUT_MS || (env.VITEST && env.OPENCLAW_TEST_HANDSHAKE_TIMEOUT_MS);
|
||||
@@ -112,6 +123,7 @@ export function getPreauthHandshakeTimeoutMsFromEnv(env: NodeJS.ProcessEnv = pro
|
||||
return DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/** Resolves the server preauth timeout from env, explicit config, or default. */
|
||||
export function resolvePreauthHandshakeTimeoutMs(params?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
configuredTimeoutMs?: number | null;
|
||||
|
||||
@@ -22,10 +22,12 @@ export const GATEWAY_CLIENT_IDS = {
|
||||
PROBE: "openclaw-probe",
|
||||
} as const;
|
||||
|
||||
/** Stable gateway client ids used on the wire during hello/connect handshakes. */
|
||||
export type GatewayClientId = (typeof GATEWAY_CLIENT_IDS)[keyof typeof GATEWAY_CLIENT_IDS];
|
||||
|
||||
// Back-compat naming (internal): these values are IDs, not display names.
|
||||
export const GATEWAY_CLIENT_NAMES = GATEWAY_CLIENT_IDS;
|
||||
/** Compatibility alias for internal callers that still use "name" terminology. */
|
||||
export type GatewayClientName = GatewayClientId;
|
||||
|
||||
export const GATEWAY_CLIENT_MODES = {
|
||||
@@ -38,8 +40,10 @@ export const GATEWAY_CLIENT_MODES = {
|
||||
TEST: "test",
|
||||
} as const;
|
||||
|
||||
/** Coarse client category used for gateway policy and diagnostics. */
|
||||
export type GatewayClientMode = (typeof GATEWAY_CLIENT_MODES)[keyof typeof GATEWAY_CLIENT_MODES];
|
||||
|
||||
/** Client metadata sent during gateway connection setup. */
|
||||
export type GatewayClientInfo = {
|
||||
id: GatewayClientId;
|
||||
displayName?: string;
|
||||
@@ -55,11 +59,13 @@ export const GATEWAY_CLIENT_CAPS = {
|
||||
TOOL_EVENTS: "tool-events",
|
||||
} as const;
|
||||
|
||||
/** Optional capability advertised by clients during gateway handshake. */
|
||||
export type GatewayClientCap = (typeof GATEWAY_CLIENT_CAPS)[keyof typeof GATEWAY_CLIENT_CAPS];
|
||||
|
||||
const GATEWAY_CLIENT_ID_SET = new Set<GatewayClientId>(Object.values(GATEWAY_CLIENT_IDS));
|
||||
const GATEWAY_CLIENT_MODE_SET = new Set<GatewayClientMode>(Object.values(GATEWAY_CLIENT_MODES));
|
||||
|
||||
/** Normalizes untrusted client ids and rejects unknown values. */
|
||||
export function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined {
|
||||
const normalized = normalizeOptionalLowercaseString(raw);
|
||||
if (!normalized) {
|
||||
@@ -70,10 +76,12 @@ export function normalizeGatewayClientId(raw?: string | null): GatewayClientId |
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Normalizes legacy client-name fields through the canonical client-id registry. */
|
||||
export function normalizeGatewayClientName(raw?: string | null): GatewayClientName | undefined {
|
||||
return normalizeGatewayClientId(raw);
|
||||
}
|
||||
|
||||
/** Normalizes untrusted client modes and rejects unknown values. */
|
||||
export function normalizeGatewayClientMode(raw?: string | null): GatewayClientMode | undefined {
|
||||
const normalized = normalizeOptionalLowercaseString(raw);
|
||||
if (!normalized) {
|
||||
@@ -84,6 +92,7 @@ export function normalizeGatewayClientMode(raw?: string | null): GatewayClientMo
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Checks a client-advertised capability list without treating missing caps as errors. */
|
||||
export function hasGatewayClientCap(
|
||||
caps: string[] | null | undefined,
|
||||
cap: GatewayClientCap,
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
/** Canonical id for file secret providers that expose exactly one value. */
|
||||
export const SINGLE_VALUE_FILE_REF_ID = "value";
|
||||
|
||||
/** Shared alias grammar for env/file/exec secret provider names. */
|
||||
export const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
||||
/** JSON-schema fragment that rejects absolute file secret ref ids. */
|
||||
export const FILE_SECRET_REF_ID_ABSOLUTE_JSON_SCHEMA_PATTERN = "^/";
|
||||
/** JSON-schema fragment that rejects invalid JSON-pointer escape sequences. */
|
||||
export const FILE_SECRET_REF_ID_INVALID_ESCAPE_JSON_SCHEMA_PATTERN = "~(?:[^01]|$)";
|
||||
/** JSON-schema pattern for exec secret ref ids, excluding dot-path traversal. */
|
||||
export const EXEC_SECRET_REF_ID_JSON_SCHEMA_PATTERN =
|
||||
"^(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9][A-Za-z0-9._:/#-]{0,255}$";
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
/** Structured error reason used while gateway startup sidecars are still initializing. */
|
||||
export const GATEWAY_STARTUP_UNAVAILABLE_REASON = "startup-sidecars";
|
||||
/** Internal close cause that distinguishes startup retry closes from generic disconnects. */
|
||||
export const GATEWAY_STARTUP_PENDING_CLOSE_CAUSE = "startup-sidecars-pending";
|
||||
/** WebSocket close code for temporary gateway unavailability. */
|
||||
export const GATEWAY_STARTUP_CLOSE_CODE = 1013;
|
||||
/** Human-readable WebSocket close reason for temporary gateway startup unavailability. */
|
||||
export const GATEWAY_STARTUP_CLOSE_REASON = "gateway starting";
|
||||
/** Default retry-after hint sent with startup-unavailable handshake errors. */
|
||||
export const GATEWAY_STARTUP_RETRY_AFTER_MS = 500;
|
||||
const GATEWAY_STARTUP_RETRY_MIN_MS = 100;
|
||||
const GATEWAY_STARTUP_RETRY_MAX_MS = 2_000;
|
||||
|
||||
/** Details payload attached to retryable startup-unavailable gateway errors. */
|
||||
export type GatewayStartupUnavailableDetails = {
|
||||
reason: typeof GATEWAY_STARTUP_UNAVAILABLE_REASON;
|
||||
};
|
||||
|
||||
/** Builds the canonical startup-unavailable details payload. */
|
||||
export function gatewayStartupUnavailableDetails(): GatewayStartupUnavailableDetails {
|
||||
return { reason: GATEWAY_STARTUP_UNAVAILABLE_REASON };
|
||||
}
|
||||
@@ -24,6 +31,7 @@ function isGatewayStartupUnavailableDetails(
|
||||
);
|
||||
}
|
||||
|
||||
/** Detects the structured retryable error emitted while startup sidecars are pending. */
|
||||
export function isRetryableGatewayStartupUnavailableError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
@@ -42,6 +50,7 @@ export function isRetryableGatewayStartupUnavailableError(error: unknown): boole
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolves a bounded retry-after delay from a startup-unavailable error. */
|
||||
export function resolveGatewayStartupRetryAfterMs(error: unknown): number | null {
|
||||
if (!isRetryableGatewayStartupUnavailableError(error)) {
|
||||
return null;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/** Current gateway protocol version emitted by modern clients and servers. */
|
||||
export const PROTOCOL_VERSION = 4 as const;
|
||||
/** Lowest client protocol version accepted by the gateway. */
|
||||
export const MIN_CLIENT_PROTOCOL_VERSION = 4 as const;
|
||||
/** Lowest lightweight probe protocol version accepted by the gateway. */
|
||||
export const MIN_PROBE_PROTOCOL_VERSION = 4 as const;
|
||||
|
||||
@@ -281,6 +281,7 @@ function formatValidationPath(error: TLocalizedValidationError): string {
|
||||
return path || "root";
|
||||
}
|
||||
|
||||
/** Finds the target tool and validates/coerces a model-emitted tool call. */
|
||||
export function validateToolCall(tools: Tool[], toolCall: ToolCall): unknown {
|
||||
const tool = tools.find((t) => t.name === toolCall.name);
|
||||
if (!tool) {
|
||||
@@ -289,6 +290,7 @@ export function validateToolCall(tools: Tool[], toolCall: ToolCall): unknown {
|
||||
return validateToolArguments(tool, toolCall);
|
||||
}
|
||||
|
||||
/** Validates tool arguments against TypeBox or plain JSON-schema parameters. */
|
||||
export function validateToolArguments(tool: Tool, toolCall: ToolCall): unknown {
|
||||
const args = structuredClone(toolCall.arguments);
|
||||
Value.Convert(tool.parameters, args);
|
||||
|
||||
@@ -8,21 +8,21 @@ import type {
|
||||
StreamOptions,
|
||||
} from "../../llm-core/src/index.js";
|
||||
|
||||
// Type-only source import keeps plugin SDK declarations self-contained; package
|
||||
// runtime emits no llm-core import from this module.
|
||||
|
||||
/** Runtime stream adapter signature stored in the API provider registry. */
|
||||
export type ApiStreamFunction = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: StreamOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
/** Runtime simple-stream adapter signature stored in the API provider registry. */
|
||||
export type ApiStreamSimpleFunction = (
|
||||
model: Model,
|
||||
context: Context,
|
||||
options?: SimpleStreamOptions,
|
||||
) => AssistantMessageEventStreamContract;
|
||||
|
||||
/** Provider implementation registered by core or plugins for a specific model API. */
|
||||
export interface ApiProvider<
|
||||
TApi extends Api = Api,
|
||||
TOptions extends StreamOptions = StreamOptions,
|
||||
@@ -69,6 +69,7 @@ function wrapStreamSimple<TApi extends Api>(
|
||||
};
|
||||
}
|
||||
|
||||
/** Registers or replaces the provider implementation for an API id. */
|
||||
export function registerApiProvider<TApi extends Api, TOptions extends StreamOptions>(
|
||||
provider: ApiProvider<TApi, TOptions>,
|
||||
sourceId?: string,
|
||||
@@ -83,14 +84,17 @@ export function registerApiProvider<TApi extends Api, TOptions extends StreamOpt
|
||||
});
|
||||
}
|
||||
|
||||
/** Looks up a registered API provider by API id. */
|
||||
export function getApiProvider(api: Api): ApiProviderInternal | undefined {
|
||||
return apiProviderRegistry.get(api)?.provider;
|
||||
}
|
||||
|
||||
/** Lists all currently registered API providers. */
|
||||
export function getApiProviders(): ApiProviderInternal[] {
|
||||
return Array.from(apiProviderRegistry.values(), (entry) => entry.provider);
|
||||
}
|
||||
|
||||
/** Removes all providers registered by a plugin/source id. */
|
||||
export function unregisterApiProviders(sourceId: string): void {
|
||||
for (const [api, entry] of apiProviderRegistry.entries()) {
|
||||
if (entry.sourceId === sourceId) {
|
||||
@@ -99,6 +103,7 @@ export function unregisterApiProviders(sourceId: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears the registry for test teardown and runtime reset flows. */
|
||||
export function clearApiProviders(): void {
|
||||
apiProviderRegistry.clear();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ function scanParenAwareBreakpoints(text: string): { lastNewline: number; lastWhi
|
||||
return { lastNewline, lastWhitespace };
|
||||
}
|
||||
|
||||
/** Splits plain text at readable boundaries while avoiding breaks inside parentheses. */
|
||||
export function chunkText(text: string, limit: number): string[] {
|
||||
const early = resolveChunkEarlyReturn(text, limit);
|
||||
if (early) {
|
||||
|
||||
@@ -17,6 +17,7 @@ export type FenceScanState = {
|
||||
};
|
||||
};
|
||||
|
||||
/** Scans fenced-code spans incrementally so chunking can carry an open fence forward. */
|
||||
export function scanFenceSpans(
|
||||
buffer: string,
|
||||
state?: FenceScanState,
|
||||
@@ -102,10 +103,12 @@ export function scanFenceSpans(
|
||||
return { spans, state: nextState };
|
||||
}
|
||||
|
||||
/** Parses all fenced-code spans in a complete markdown buffer. */
|
||||
export function parseFenceSpans(buffer: string): FenceSpan[] {
|
||||
return scanFenceSpans(buffer).spans;
|
||||
}
|
||||
|
||||
/** Looks up the fence containing an offset; spans must be sorted by start offset. */
|
||||
export function findFenceSpanAt(spans: FenceSpan[], index: number): FenceSpan | undefined {
|
||||
let low = 0;
|
||||
let high = spans.length - 1;
|
||||
@@ -130,6 +133,7 @@ export function findFenceSpanAt(spans: FenceSpan[], index: number): FenceSpan |
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** True when a chunk boundary would not split a fenced-code block. */
|
||||
export function isSafeFenceBreak(spans: FenceSpan[], index: number): boolean {
|
||||
return !findFenceSpanAt(spans, index);
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ function extractFrontmatterBlock(content: string): string | undefined {
|
||||
return normalized.slice(4, endIndex);
|
||||
}
|
||||
|
||||
/** Parses leading YAML frontmatter into string values used by skill and metadata loaders. */
|
||||
export function parseFrontmatterBlock(content: string): ParsedFrontmatter {
|
||||
const block = extractFrontmatterBlock(content);
|
||||
if (!block) {
|
||||
|
||||
@@ -10,6 +10,7 @@ const MARKDOWN_STYLE_MARKERS = {
|
||||
code_block: { open: "```\n", close: "```" },
|
||||
} as const;
|
||||
|
||||
/** Converts markdown tables into the configured plaintext/code rendering mode. */
|
||||
export function convertMarkdownTables(markdown: string, mode: MarkdownTableMode): string {
|
||||
if (!markdown || mode === "off") {
|
||||
return markdown;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { normalizeOptionalString } from "./string.js";
|
||||
|
||||
/** Provider catalog entry shape used when resolving capability-scoped model references. */
|
||||
export type CapabilityModelProviderCandidate = {
|
||||
id: string;
|
||||
aliases?: readonly string[];
|
||||
@@ -7,6 +8,7 @@ export type CapabilityModelProviderCandidate = {
|
||||
models?: readonly string[];
|
||||
};
|
||||
|
||||
/** Normalized provider/model reference selected for a media capability. */
|
||||
export type CapabilityModelRef = {
|
||||
provider: string;
|
||||
model: string;
|
||||
@@ -22,6 +24,7 @@ function normalizeProviderForMatch(
|
||||
return normalized && normalizeProviderId ? normalizeProviderId(normalized) : normalized;
|
||||
}
|
||||
|
||||
/** Finds a provider by id or alias using the caller's provider-id normalization rules. */
|
||||
export function findCapabilityProviderById<T extends CapabilityModelProviderCandidate>(params: {
|
||||
providers: readonly T[];
|
||||
providerId?: string;
|
||||
@@ -43,6 +46,7 @@ export function findCapabilityProviderById<T extends CapabilityModelProviderCand
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolves a bare model name to the provider that advertises it for this capability. */
|
||||
export function resolveCapabilityProviderModelOnlyRef(params: {
|
||||
providers: readonly CapabilityModelProviderCandidate[];
|
||||
raw?: string;
|
||||
@@ -58,6 +62,7 @@ export function resolveCapabilityProviderModelOnlyRef(params: {
|
||||
return provider ? { provider: provider.id, model } : null;
|
||||
}
|
||||
|
||||
/** Resolves provider/model refs first, then falls back to model-only catalog matching. */
|
||||
export function resolveCapabilityModelRefForProviders(params: {
|
||||
providers: readonly CapabilityModelProviderCandidate[];
|
||||
raw?: string;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { normalizeOptionalString } from "./string.js";
|
||||
|
||||
/** Provider/model pair parsed from a generation model reference like `provider/model`. */
|
||||
export type ParsedGenerationModelRef = {
|
||||
provider: string;
|
||||
model: string;
|
||||
};
|
||||
|
||||
/** Parses strict generation model refs and rejects missing provider or model segments. */
|
||||
export function parseGenerationModelRef(raw: string | undefined): ParsedGenerationModelRef | null {
|
||||
const trimmed = normalizeOptionalString(raw);
|
||||
if (!trimmed) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Primitive value types reported in media generation normalization metadata. */
|
||||
export type MediaNormalizationValue = string | number | boolean;
|
||||
|
||||
/** Requested/applied value pair plus provenance for a normalized media option. */
|
||||
export type MediaNormalizationEntry<TValue extends MediaNormalizationValue> = {
|
||||
requested?: TValue;
|
||||
applied?: TValue;
|
||||
@@ -7,6 +9,7 @@ export type MediaNormalizationEntry<TValue extends MediaNormalizationValue> = {
|
||||
supportedValues?: readonly TValue[];
|
||||
};
|
||||
|
||||
/** Normalization metadata shared by media generation responses. */
|
||||
export type MediaGenerationNormalizationMetadataInput = {
|
||||
size?: MediaNormalizationEntry<string>;
|
||||
aspectRatio?: MediaNormalizationEntry<string>;
|
||||
@@ -14,6 +17,7 @@ export type MediaGenerationNormalizationMetadataInput = {
|
||||
durationSeconds?: MediaNormalizationEntry<number>;
|
||||
};
|
||||
|
||||
/** True when a normalization entry contains any user-visible normalization metadata. */
|
||||
export function hasMediaNormalizationEntry<TValue extends MediaNormalizationValue>(
|
||||
entry: MediaNormalizationEntry<TValue> | undefined,
|
||||
): entry is MediaNormalizationEntry<TValue> {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { MediaUnderstandingOutput } from "./types.js";
|
||||
const MEDIA_PLACEHOLDER_RE = /^<media:[^>]+>(\s*\([^)]*\))?$/i;
|
||||
const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
|
||||
|
||||
/** Extracts user-authored text while ignoring synthetic media placeholder tokens. */
|
||||
export function extractMediaUserText(body?: string): string | undefined {
|
||||
const trimmed = body?.trim() ?? "";
|
||||
if (!trimmed) {
|
||||
@@ -29,6 +30,7 @@ function formatSection(
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Formats media-understanding outputs into the chat body sent back to the model. */
|
||||
export function formatMediaUnderstandingBody(params: {
|
||||
body?: string;
|
||||
outputs: MediaUnderstandingOutput[];
|
||||
@@ -90,6 +92,7 @@ export function formatMediaUnderstandingBody(params: {
|
||||
return sections.join("\n\n").trim();
|
||||
}
|
||||
|
||||
/** Formats one or more audio transcript outputs for legacy transcript-only callers. */
|
||||
export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string {
|
||||
if (outputs.length === 1) {
|
||||
return outputs[0].text;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/** Returns a number only when the input is already finite. */
|
||||
export function asFiniteNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
/** Returns a finite number only when it satisfies the supplied inclusive/exclusive bounds. */
|
||||
export function asFiniteNumberInRange(
|
||||
value: unknown,
|
||||
range: {
|
||||
@@ -28,6 +30,7 @@ export function asFiniteNumberInRange(
|
||||
return number;
|
||||
}
|
||||
|
||||
/** Returns a safe integer only when it satisfies the supplied inclusive bounds. */
|
||||
export function asSafeIntegerInRange(
|
||||
value: unknown,
|
||||
range: {
|
||||
@@ -52,6 +55,7 @@ function normalizeNumericString(value: string): string | undefined {
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
/** Parses finite numbers from number values or strict numeric string tokens. */
|
||||
export function parseFiniteNumber(value: unknown): number | undefined {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
@@ -59,6 +63,7 @@ export function parseFiniteNumber(value: unknown): number | undefined {
|
||||
return parseStrictFiniteNumber(value);
|
||||
}
|
||||
|
||||
/** Parses only safe integer numbers or base-10 integer strings. */
|
||||
export function parseStrictInteger(value: unknown): number | undefined {
|
||||
if (typeof value === "number") {
|
||||
return Number.isSafeInteger(value) ? value : undefined;
|
||||
@@ -74,6 +79,7 @@ export function parseStrictInteger(value: unknown): number | undefined {
|
||||
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
/** Parses only finite decimal/scientific string tokens, rejecting partial numbers. */
|
||||
export function parseStrictFiniteNumber(value: unknown): number | undefined {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : undefined;
|
||||
@@ -89,15 +95,21 @@ export function parseStrictFiniteNumber(value: unknown): number | undefined {
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
/** Returns positive safe integers without string coercion. */
|
||||
export function asPositiveSafeInteger(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
/** Conservative upper bound for Node timer delays. */
|
||||
export const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
|
||||
/** Timer bound expressed in whole seconds for env/config inputs. */
|
||||
export const MAX_TIMER_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_TIMEOUT_MS / 1000);
|
||||
/** Largest timestamp accepted by JavaScript Date. */
|
||||
export const MAX_DATE_TIMESTAMP_MS = 8_640_000_000_000_000;
|
||||
/** Fallback ISO value for invalid timestamp inputs. */
|
||||
export const UNIX_EPOCH_ISO_STRING = "1970-01-01T00:00:00.000Z";
|
||||
|
||||
/** Returns a Date-valid millisecond timestamp. */
|
||||
export function asDateTimestampMs(value: unknown): number | undefined {
|
||||
return asFiniteNumberInRange(value, {
|
||||
min: -MAX_DATE_TIMESTAMP_MS,
|
||||
@@ -105,6 +117,7 @@ export function asDateTimestampMs(value: unknown): number | undefined {
|
||||
});
|
||||
}
|
||||
|
||||
/** Checks whether a Date-valid timestamp is after the supplied/current time. */
|
||||
export function isFutureDateTimestampMs(
|
||||
value: unknown,
|
||||
opts: { nowMs?: number } = {},
|
||||
@@ -114,11 +127,13 @@ export function isFutureDateTimestampMs(
|
||||
return timestampMs !== undefined && nowMs !== undefined && timestampMs > nowMs;
|
||||
}
|
||||
|
||||
/** Converts Date-valid millisecond timestamps to ISO strings. */
|
||||
export function timestampMsToIsoString(value: unknown): string | undefined {
|
||||
const timestampMs = asDateTimestampMs(value);
|
||||
return timestampMs === undefined ? undefined : new Date(timestampMs).toISOString();
|
||||
}
|
||||
|
||||
/** Resolves a Date-valid timestamp with a Date-valid fallback. */
|
||||
export function resolveDateTimestampMs(
|
||||
value: unknown,
|
||||
fallbackValue: unknown = Date.now(),
|
||||
@@ -126,6 +141,7 @@ export function resolveDateTimestampMs(
|
||||
return asDateTimestampMs(value) ?? asDateTimestampMs(fallbackValue) ?? 0;
|
||||
}
|
||||
|
||||
/** Resolves a Date-valid timestamp to ISO, falling back to Unix epoch if needed. */
|
||||
export function resolveTimestampMsToIsoString(
|
||||
value: unknown,
|
||||
fallbackValue: unknown = Date.now(),
|
||||
@@ -135,6 +151,7 @@ export function resolveTimestampMsToIsoString(
|
||||
);
|
||||
}
|
||||
|
||||
/** Formats Date-valid timestamps for filenames by replacing colon separators. */
|
||||
export function timestampMsToIsoFileStamp(
|
||||
value: unknown,
|
||||
fallbackValue: unknown = Date.now(),
|
||||
@@ -142,6 +159,7 @@ export function timestampMsToIsoFileStamp(
|
||||
return resolveTimestampMsToIsoString(value, fallbackValue).replaceAll(":", "-");
|
||||
}
|
||||
|
||||
/** Clamps finite millisecond values into the Node-safe timer range. */
|
||||
export function clampTimerTimeoutMs(valueMs: unknown, minMs = 1): number | undefined {
|
||||
const value = asFiniteNumber(valueMs);
|
||||
if (value === undefined) {
|
||||
@@ -151,6 +169,7 @@ export function clampTimerTimeoutMs(valueMs: unknown, minMs = 1): number | undef
|
||||
return Math.min(Math.max(Math.floor(value), min), MAX_TIMER_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/** Clamps positive finite millisecond values into the Node-safe timer range. */
|
||||
export function clampPositiveTimerTimeoutMs(valueMs: unknown): number | undefined {
|
||||
const value = asFiniteNumber(valueMs);
|
||||
if (value === undefined || value <= 0) {
|
||||
@@ -159,10 +178,12 @@ export function clampPositiveTimerTimeoutMs(valueMs: unknown): number | undefine
|
||||
return clampTimerTimeoutMs(value);
|
||||
}
|
||||
|
||||
/** Resolves a positive timer timeout or falls back through safe timer clamping. */
|
||||
export function resolvePositiveTimerTimeoutMs(valueMs: unknown, fallbackMs: number): number {
|
||||
return clampPositiveTimerTimeoutMs(valueMs) ?? resolveTimerTimeoutMs(fallbackMs, 1);
|
||||
}
|
||||
|
||||
/** Resolves arbitrary timeout input with fallback and minimum timer bounds. */
|
||||
export function resolveTimerTimeoutMs(valueMs: unknown, fallbackMs: number, minMs = 1): number {
|
||||
const value = asFiniteNumber(valueMs) ?? asFiniteNumber(fallbackMs);
|
||||
const min = Math.max(0, Math.floor(minMs));
|
||||
@@ -172,6 +193,7 @@ export function resolveTimerTimeoutMs(valueMs: unknown, fallbackMs: number, minM
|
||||
return Math.min(Math.max(Math.floor(value), min), MAX_TIMER_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/** Adds grace time to a finite timeout and clamps the result to Node-safe bounds. */
|
||||
export function addTimerTimeoutGraceMs(timeoutMs: unknown, graceMs = 5_000): number | undefined {
|
||||
const timeout = asFiniteNumber(timeoutMs);
|
||||
const grace = asFiniteNumber(graceMs);
|
||||
@@ -182,6 +204,7 @@ export function addTimerTimeoutGraceMs(timeoutMs: unknown, graceMs = 5_000): num
|
||||
return Number.isFinite(withGrace) ? clampTimerTimeoutMs(withGrace) : MAX_TIMER_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/** Converts finite positive seconds to Node-safe milliseconds. */
|
||||
export function finiteSecondsToTimerSafeMilliseconds(
|
||||
value: unknown,
|
||||
opts: { floorSeconds?: boolean } = {},
|
||||
@@ -198,6 +221,7 @@ export function finiteSecondsToTimerSafeMilliseconds(
|
||||
return Math.min(milliseconds, MAX_TIMER_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/** Resolves an integer option from finite numeric input or fallback, then clamps bounds. */
|
||||
export function resolveIntegerOption(
|
||||
value: unknown,
|
||||
fallback: number,
|
||||
@@ -212,6 +236,7 @@ export function resolveIntegerOption(
|
||||
return range.max === undefined ? minBounded : Math.min(range.max, minBounded);
|
||||
}
|
||||
|
||||
/** Resolves an optional integer option, returning undefined for non-finite input. */
|
||||
export function resolveOptionalIntegerOption(
|
||||
value: unknown,
|
||||
range: {
|
||||
@@ -225,20 +250,24 @@ export function resolveOptionalIntegerOption(
|
||||
return resolveIntegerOption(value, value, range);
|
||||
}
|
||||
|
||||
/** Resolves an integer option with a non-negative lower bound. */
|
||||
export function resolveNonNegativeIntegerOption(value: unknown, fallback: number): number {
|
||||
return resolveIntegerOption(value, fallback, { min: 0 });
|
||||
}
|
||||
|
||||
/** Parses strict positive integer values from numbers or strings. */
|
||||
export function parseStrictPositiveInteger(value: unknown): number | undefined {
|
||||
const parsed = parseStrictInteger(value);
|
||||
return parsed !== undefined && parsed > 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
/** Parses strict non-negative integer values from numbers or strings. */
|
||||
export function parseStrictNonNegativeInteger(value: unknown): number | undefined {
|
||||
const parsed = parseStrictInteger(value);
|
||||
return parsed !== undefined && parsed >= 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
/** Converts strict positive seconds to safe millisecond counts. */
|
||||
export function positiveSecondsToSafeMilliseconds(value: unknown): number | undefined {
|
||||
const seconds = parseStrictPositiveInteger(value);
|
||||
if (seconds === undefined) {
|
||||
@@ -248,6 +277,7 @@ export function positiveSecondsToSafeMilliseconds(value: unknown): number | unde
|
||||
return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
|
||||
}
|
||||
|
||||
/** Converts strict non-negative seconds to safe millisecond counts. */
|
||||
export function nonNegativeSecondsToSafeMilliseconds(value: unknown): number | undefined {
|
||||
const seconds = parseStrictNonNegativeInteger(value);
|
||||
if (seconds === undefined) {
|
||||
@@ -257,6 +287,7 @@ export function nonNegativeSecondsToSafeMilliseconds(value: unknown): number | u
|
||||
return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
|
||||
}
|
||||
|
||||
/** Resolves an absolute expiration timestamp from a positive duration in milliseconds. */
|
||||
export function resolveExpiresAtMsFromDurationMs(
|
||||
value: unknown,
|
||||
opts: { nowMs?: number; bufferMs?: number; minRemainingMs?: number } = {},
|
||||
@@ -285,6 +316,7 @@ export function resolveExpiresAtMsFromDurationMs(
|
||||
return Math.max(expiresAt, minExpiresAt);
|
||||
}
|
||||
|
||||
/** Resolves an absolute expiration timestamp from a positive duration in seconds. */
|
||||
export function resolveExpiresAtMsFromDurationSeconds(
|
||||
value: unknown,
|
||||
opts: { nowMs?: number; bufferMs?: number; minRemainingMs?: number } = {},
|
||||
@@ -293,6 +325,7 @@ export function resolveExpiresAtMsFromDurationSeconds(
|
||||
return durationMs === undefined ? undefined : resolveExpiresAtMsFromDurationMs(durationMs, opts);
|
||||
}
|
||||
|
||||
/** Resolves an absolute expiration timestamp from Unix epoch seconds. */
|
||||
export function resolveExpiresAtMsFromEpochSeconds(
|
||||
value: unknown,
|
||||
opts: { bufferMs?: number; maxMs?: number } = {},
|
||||
@@ -315,6 +348,7 @@ export function resolveExpiresAtMsFromEpochSeconds(
|
||||
return maxMs === undefined || expiresAt <= maxMs ? expiresAt : undefined;
|
||||
}
|
||||
|
||||
/** Resolves expiration input that may be relative seconds, epoch seconds, or epoch milliseconds. */
|
||||
export function resolveExpiresAtMsFromDurationOrEpoch(
|
||||
value: unknown,
|
||||
opts: {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// Keep this local so browser bundles do not pull in src/utils.ts and its Node-only side effects.
|
||||
/** Type guard for non-array object records at browser-safe boundaries. */
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/** Coerces object-like values to records, falling back to an empty record. */
|
||||
export function asRecord(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
/** Reads a field only when it exists as a string. */
|
||||
export function readStringField(
|
||||
record: Record<string, unknown> | null | undefined,
|
||||
key: string,
|
||||
@@ -15,18 +18,22 @@ export function readStringField(
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
/** Returns a non-array record or undefined. */
|
||||
export function asOptionalRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return isRecord(value) ? value : undefined;
|
||||
}
|
||||
|
||||
/** Returns a non-array record or null. */
|
||||
export function asNullableRecord(value: unknown): Record<string, unknown> | null {
|
||||
return isRecord(value) ? value : null;
|
||||
}
|
||||
|
||||
/** Returns any object-backed record, including arrays, or undefined. */
|
||||
export function asOptionalObjectRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||
}
|
||||
|
||||
/** Returns any object-backed record, including arrays, or null. */
|
||||
export function asNullableObjectRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/** Reads a value only when it is already a string, preserving whitespace. */
|
||||
export function readStringValue(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
/** Trims string input and returns null for non-strings or empty strings. */
|
||||
export function normalizeNullableString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
@@ -10,10 +12,12 @@ export function normalizeNullableString(value: unknown): string | null {
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
/** Trims string input and returns undefined for non-strings or empty strings. */
|
||||
export function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return normalizeNullableString(value) ?? undefined;
|
||||
}
|
||||
|
||||
/** Stringifies primitive ids/flags before applying optional string normalization. */
|
||||
export function normalizeStringifiedOptionalString(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return normalizeOptionalString(value);
|
||||
@@ -24,20 +28,24 @@ export function normalizeStringifiedOptionalString(value: unknown): string | und
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Normalizes an optional array of primitive-ish values into non-empty strings. */
|
||||
export function normalizeStringifiedEntries(values?: ReadonlyArray<unknown>): string[] {
|
||||
return (values ?? [])
|
||||
.map((entry) => normalizeStringifiedOptionalString(entry))
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
}
|
||||
|
||||
/** Lowercases a normalized optional string. */
|
||||
export function normalizeOptionalLowercaseString(value: unknown): string | undefined {
|
||||
return normalizeOptionalString(value)?.toLowerCase();
|
||||
}
|
||||
|
||||
/** Lowercases a normalized string or returns an empty string when absent. */
|
||||
export function normalizeLowercaseStringOrEmpty(value: unknown): string {
|
||||
return normalizeOptionalLowercaseString(value) ?? "";
|
||||
}
|
||||
|
||||
/** Parses loose boolean/fast-mode flags from strings or booleans. */
|
||||
export function normalizeFastMode(raw?: string | boolean | null): boolean | undefined {
|
||||
if (typeof raw === "boolean") {
|
||||
return raw;
|
||||
@@ -55,14 +63,17 @@ export function normalizeFastMode(raw?: string | boolean | null): boolean | unde
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Lowercases text while intentionally preserving surrounding whitespace. */
|
||||
export function lowercasePreservingWhitespace(value: string): string {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
|
||||
/** Locale-aware lowercase helper that still preserves surrounding whitespace. */
|
||||
export function localeLowercasePreservingWhitespace(value: string): string {
|
||||
return value.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
/** Reads a string directly or from an object's `primary` field. */
|
||||
export function resolvePrimaryStringValue(value: unknown): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return normalizeOptionalString(value);
|
||||
@@ -73,6 +84,7 @@ export function resolvePrimaryStringValue(value: unknown): string | undefined {
|
||||
return normalizeOptionalString((value as { primary?: unknown }).primary);
|
||||
}
|
||||
|
||||
/** Normalizes thread ids that may be numeric or string-backed. */
|
||||
export function normalizeOptionalThreadValue(value: unknown): string | number | undefined {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? Math.trunc(value) : undefined;
|
||||
@@ -80,11 +92,13 @@ export function normalizeOptionalThreadValue(value: unknown): string | number |
|
||||
return normalizeOptionalString(value);
|
||||
}
|
||||
|
||||
/** Normalizes a thread/id value and stringifies finite numeric ids. */
|
||||
export function normalizeOptionalStringifiedId(value: unknown): string | undefined {
|
||||
const normalized = normalizeOptionalThreadValue(value);
|
||||
return normalized == null ? undefined : String(normalized);
|
||||
}
|
||||
|
||||
/** Type guard for strings that remain non-empty after trimming. */
|
||||
export function hasNonEmptyString(value: unknown): value is string {
|
||||
return normalizeOptionalString(value) !== undefined;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
import { normalizeOptionalLowercaseString, normalizeOptionalString } from "./string-coerce.js";
|
||||
|
||||
/** Coerces entries to strings, trims them, and drops empty results. */
|
||||
export function normalizeStringEntries(list?: ReadonlyArray<unknown>) {
|
||||
return (list ?? []).map((entry) => normalizeOptionalString(String(entry)) ?? "").filter(Boolean);
|
||||
}
|
||||
|
||||
/** Normalizes string entries and lowercases each retained value. */
|
||||
export function normalizeStringEntriesLower(list?: ReadonlyArray<unknown>) {
|
||||
return normalizeStringEntries(list).map((entry) => normalizeOptionalLowercaseString(entry) ?? "");
|
||||
}
|
||||
|
||||
/** Returns first-seen unique values while preserving insertion order. */
|
||||
export function uniqueValues<T>(values: Iterable<T>): T[] {
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
/** Returns first-seen unique strings while preserving insertion order. */
|
||||
export function uniqueStrings(values: Iterable<string>): string[] {
|
||||
return uniqueValues(values);
|
||||
}
|
||||
|
||||
/** Returns unique strings sorted with stable ASCII comparison. */
|
||||
export function sortUniqueStrings(values: Iterable<string>): string[] {
|
||||
return uniqueStrings(values).toSorted((left, right) =>
|
||||
left < right ? -1 : left > right ? 1 : 0,
|
||||
);
|
||||
}
|
||||
|
||||
/** Normalizes entries, removes duplicates, and preserves first-seen order. */
|
||||
export function normalizeUniqueStringEntries(values?: Iterable<unknown>): string[] {
|
||||
return uniqueStrings(normalizeStringEntries(values ? [...values] : undefined));
|
||||
}
|
||||
|
||||
/** Lowercases normalized entries, removes empties/duplicates, and preserves first-seen order. */
|
||||
export function normalizeUniqueStringEntriesLower(values?: Iterable<unknown>): string[] {
|
||||
return uniqueStrings(
|
||||
normalizeStringEntriesLower(values ? [...values] : undefined).filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
/** Normalizes entries, removes duplicates, and returns sorted output. */
|
||||
export function normalizeSortedUniqueStringEntries(values?: Iterable<unknown>): string[] {
|
||||
return sortUniqueStrings(normalizeUniqueStringEntries(values));
|
||||
}
|
||||
|
||||
/** Normalizes array-backed string lists and rejects non-array input as empty. */
|
||||
export function normalizeTrimmedStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
@@ -46,19 +55,23 @@ export function normalizeTrimmedStringList(value: unknown): string[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** Normalizes an array-backed string list and removes duplicates. */
|
||||
export function normalizeUniqueTrimmedStringList(value: unknown): string[] {
|
||||
return uniqueStrings(normalizeTrimmedStringList(value));
|
||||
}
|
||||
|
||||
/** Normalizes an array-backed string list, removes duplicates, and sorts it. */
|
||||
export function normalizeSortedUniqueTrimmedStringList(value: unknown): string[] {
|
||||
return sortUniqueStrings(normalizeTrimmedStringList(value));
|
||||
}
|
||||
|
||||
/** Returns undefined instead of an empty normalized array-backed string list. */
|
||||
export function normalizeOptionalTrimmedStringList(value: unknown): string[] | undefined {
|
||||
const normalized = normalizeTrimmedStringList(value);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
/** Returns undefined for non-arrays but preserves an empty array for explicit arrays. */
|
||||
export function normalizeArrayBackedTrimmedStringList(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
@@ -66,6 +79,7 @@ export function normalizeArrayBackedTrimmedStringList(value: unknown): string[]
|
||||
return normalizeTrimmedStringList(value);
|
||||
}
|
||||
|
||||
/** Normalizes either a single string-like value or an array-backed string list. */
|
||||
export function normalizeSingleOrTrimmedStringList(value: unknown): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return normalizeTrimmedStringList(value);
|
||||
@@ -74,10 +88,12 @@ export function normalizeSingleOrTrimmedStringList(value: unknown): string[] {
|
||||
return normalized ? [normalized] : [];
|
||||
}
|
||||
|
||||
/** Normalizes single-or-array string input and removes duplicates. */
|
||||
export function normalizeUniqueSingleOrTrimmedStringList(value: unknown): string[] {
|
||||
return uniqueStrings(normalizeSingleOrTrimmedStringList(value));
|
||||
}
|
||||
|
||||
/** Parses either array entries or comma-separated string entries into trimmed values. */
|
||||
export function normalizeCsvOrLooseStringList(value: unknown): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return normalizeStringEntries(value);
|
||||
@@ -95,6 +111,7 @@ function normalizeSlugInput(raw?: string | null) {
|
||||
return (normalizeOptionalLowercaseString(raw) ?? "").normalize("NFC");
|
||||
}
|
||||
|
||||
/** Normalizes user-facing names into permissive lowercase hyphen slugs. */
|
||||
export function normalizeHyphenSlug(raw?: string | null) {
|
||||
const trimmed = normalizeSlugInput(raw);
|
||||
if (!trimmed) {
|
||||
@@ -105,6 +122,7 @@ export function normalizeHyphenSlug(raw?: string | null) {
|
||||
return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
||||
}
|
||||
|
||||
/** Normalizes @/#-prefixed names into lowercase hyphen slugs without the prefix. */
|
||||
export function normalizeAtHashSlug(raw?: string | null) {
|
||||
const trimmed = normalizeSlugInput(raw);
|
||||
if (!trimmed) {
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
/** Legacy marker some models emit after a serialized JSON tool request. */
|
||||
export const END_TOOL_REQUEST = "[END_TOOL_REQUEST]";
|
||||
/** Harmony stream marker that introduces the target channel before a tool call. */
|
||||
export const HARMONY_CHANNEL_MARKER = "<|channel|>";
|
||||
/** Harmony stream marker that may separate the header from the JSON payload. */
|
||||
export const HARMONY_MESSAGE_MARKER = "<|message|>";
|
||||
/** Harmony stream marker that may close a serialized tool-call payload. */
|
||||
export const HARMONY_CALL_MARKER = "<|call|>";
|
||||
|
||||
/** Accepts either a complete literal or a still-streaming prefix of that literal. */
|
||||
export function matchesLiteralPrefix(text: string, literal: string): boolean {
|
||||
return literal.startsWith(text) || text.startsWith(literal);
|
||||
}
|
||||
|
||||
/** Tool names in bracket/plain-text repairs intentionally match provider-safe ids only. */
|
||||
export function isPlainTextToolNameChar(char: string | undefined): boolean {
|
||||
return Boolean(char && /[A-Za-z0-9_-]/.test(char));
|
||||
}
|
||||
|
||||
/** XML-ish function tags allow namespace punctuation used by some model families. */
|
||||
export function isXmlishNameChar(char: string | undefined): boolean {
|
||||
return Boolean(char && /[A-Za-z0-9_.:-]/.test(char));
|
||||
}
|
||||
|
||||
/** Skips spaces and tabs only, preserving line boundaries for grammar decisions. */
|
||||
export function skipHorizontalWhitespace(text: string, start: number): number {
|
||||
let index = start;
|
||||
while (index < text.length && (text[index] === " " || text[index] === "\t")) {
|
||||
@@ -23,6 +31,7 @@ export function skipHorizontalWhitespace(text: string, start: number): number {
|
||||
return index;
|
||||
}
|
||||
|
||||
/** Skips all JavaScript whitespace when line structure is no longer meaningful. */
|
||||
export function skipWhitespace(text: string, start: number): number {
|
||||
let index = start;
|
||||
while (index < text.length && /\s/.test(text[index] ?? "")) {
|
||||
@@ -31,6 +40,7 @@ export function skipWhitespace(text: string, start: number): number {
|
||||
return index;
|
||||
}
|
||||
|
||||
/** Consumes either Unix or Windows line endings and returns the first offset after them. */
|
||||
export function consumeLineBreak(text: string, start: number): number | null {
|
||||
if (text[start] === "\r") {
|
||||
return text[start + 1] === "\n" ? start + 2 : start + 1;
|
||||
@@ -41,6 +51,7 @@ export function consumeLineBreak(text: string, start: number): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Finds the exclusive end offset of a balanced JSON object starting at `start`. */
|
||||
export function findJsonObjectEnd(
|
||||
text: string,
|
||||
start: number,
|
||||
@@ -82,11 +93,13 @@ export function findJsonObjectEnd(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Consumes one optional line break after a repaired serialized tool-call fragment. */
|
||||
export function skipSerializedToolCallTrailingLineBreak(text: string, cursor: number): number {
|
||||
const afterLineBreak = consumeLineBreak(text, cursor);
|
||||
return afterLineBreak ?? cursor;
|
||||
}
|
||||
|
||||
/** Accepts the legacy closing markers models append after JSON tool-call payloads. */
|
||||
export function consumeJsonToolClosingMarker(text: string, cursor: number): number {
|
||||
let markerStart = cursor;
|
||||
while (markerStart < text.length && /\s/.test(text[markerStart] ?? "")) {
|
||||
@@ -106,6 +119,7 @@ export function consumeJsonToolClosingMarker(text: string, cursor: number): numb
|
||||
return skipSerializedToolCallTrailingLineBreak(text, cursor);
|
||||
}
|
||||
|
||||
/** Finds JSON after bracketed tool syntax such as `[tool_name]\n{...}`. */
|
||||
export function findBracketedJsonPayloadStart(text: string): number | null {
|
||||
if (!text.startsWith("[")) {
|
||||
return null;
|
||||
@@ -121,6 +135,7 @@ export function findBracketedJsonPayloadStart(text: string): number | null {
|
||||
return text[cursor] === "{" ? cursor : null;
|
||||
}
|
||||
|
||||
/** Finds JSON after Harmony channel/tool headers while tolerating optional message markers. */
|
||||
export function findHarmonyJsonPayloadStart(text: string): number | null {
|
||||
let cursor = 0;
|
||||
if (text.startsWith(HARMONY_CHANNEL_MARKER)) {
|
||||
@@ -158,6 +173,7 @@ export function findHarmonyJsonPayloadStart(text: string): number | null {
|
||||
return text[cursor] === "{" ? cursor : null;
|
||||
}
|
||||
|
||||
/** Case-insensitive marker compare for ASCII protocol tags without locale rules. */
|
||||
export function startsWithAsciiMarkerIgnoreCase(
|
||||
text: string,
|
||||
cursor: number,
|
||||
@@ -166,6 +182,7 @@ export function startsWithAsciiMarkerIgnoreCase(
|
||||
return text.slice(cursor, cursor + marker.length).toLowerCase() === marker;
|
||||
}
|
||||
|
||||
/** Case-insensitive marker search for ASCII protocol tags without allocating regexes. */
|
||||
export function indexOfAsciiMarkerIgnoreCase(text: string, marker: string, start: number): number {
|
||||
let cursor = start;
|
||||
while (cursor < text.length) {
|
||||
@@ -181,6 +198,7 @@ export function indexOfAsciiMarkerIgnoreCase(text: string, marker: string, start
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Returns the end offset for a complete XML-ish or bracketed plain-text tool call. */
|
||||
export function findXmlishToolCallEnd(text: string): number | null {
|
||||
let cursor: number;
|
||||
const xmlFunction = /^<function=[A-Za-z0-9_.:-]+>/i.exec(text);
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { parseStandalonePlainTextToolCallBlocks, type PlainTextToolCallBlock } from "./payload.js";
|
||||
|
||||
/** Resolves model-emitted tool names to the exact names allowed by the provider request. */
|
||||
export type ToolCallRepairNameResolver = (
|
||||
rawName: string,
|
||||
allowedToolNames: Set<string>,
|
||||
) => string | null;
|
||||
|
||||
/** Builds a provider-native tool-call block from a repaired plain-text payload. */
|
||||
export type PromotedPlainTextToolCallBlockFactory = (
|
||||
block: PlainTextToolCallBlock,
|
||||
resolvedName: string,
|
||||
) => Record<string, unknown>;
|
||||
|
||||
/** Controls when standalone assistant text may be rewritten as tool-call content. */
|
||||
export type PlainTextToolCallPromotionOptions = {
|
||||
allowedStopReasons?: ReadonlySet<unknown>;
|
||||
allowedToolNames: Set<string>;
|
||||
@@ -70,6 +73,8 @@ function createTextPartPromotionCandidates(
|
||||
textParts: readonly string[],
|
||||
exactText: string,
|
||||
): string[] {
|
||||
// Some providers split structural markers across text parts; try repaired and exact joins
|
||||
// before falling back to newline joins so valid payloads promote without changing content.
|
||||
const repairedText = joinTextPartsWithStructuralLineBreaks(textParts).trim();
|
||||
const newlineJoinedText = textParts.join("\n").trim();
|
||||
return [...new Set([repairedText, exactText, newlineJoinedText].filter(Boolean))];
|
||||
@@ -111,6 +116,7 @@ function shouldPromoteMessage(options: PlainTextToolCallPromotionOptions): boole
|
||||
return !options.allowedStopReasons || options.allowedStopReasons.has(messageRecord.stopReason);
|
||||
}
|
||||
|
||||
/** Extracts candidate standalone tool-call text while rejecting mixed unsafe content. */
|
||||
export function extractStandalonePlainTextToolCallText(params: {
|
||||
allowOtherNonTextBlocks?: boolean;
|
||||
allowedStopReasons?: ReadonlySet<unknown>;
|
||||
@@ -163,6 +169,7 @@ export function extractStandalonePlainTextToolCallText(params: {
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
/** Promotes standalone plain-text tool-call messages into provider-native content blocks. */
|
||||
export function promoteStandalonePlainTextToolCallMessage(
|
||||
options: PlainTextToolCallPromotionOptions,
|
||||
): Record<string, unknown> | undefined {
|
||||
|
||||
Reference in New Issue
Block a user