docs: clarify tool result details persistence

This commit is contained in:
Peter Steinberger
2026-04-25 19:47:11 +01:00
parent fecf1e9b8f
commit 599ae7fed8
3 changed files with 37 additions and 0 deletions

View File

@@ -77,6 +77,19 @@ gateway-backed session transcript, so they are the source of truth.
Details: [Session management](/concepts/session).
## Tool result metadata
Tool result `content` is the model-visible result. Tool result `details` is
runtime metadata for UI rendering, diagnostics, media delivery, and plugins.
OpenClaw keeps that boundary explicit:
- `toolResult.details` is stripped before provider replay and compaction input.
- Persisted session transcripts keep only bounded `details`; oversized metadata
is replaced with a compact summary marked `persistedDetailsTruncated: true`.
- Plugins and tools should put text the model must read in `content`, not only
in `details`.
## Inbound bodies and history context
OpenClaw separates the **prompt body** from the **command body**:

View File

@@ -147,6 +147,21 @@ Rules:
- `onResolution` receives the resolved approval decision — `allow-once`,
`allow-always`, `deny`, `timeout`, or `cancelled`.
### Tool result persistence
Tool results can include structured `details` for UI rendering, diagnostics,
media routing, or plugin-owned metadata. Treat `details` as runtime metadata,
not prompt content:
- OpenClaw strips `toolResult.details` before provider replay and compaction
input so metadata does not become model context.
- Persisted session entries keep only bounded `details`. Oversized details are
replaced with a compact summary and `persistedDetailsTruncated: true`.
- `tool_result_persist` and `before_message_write` run before the final
persistence cap. Hooks should still keep returned `details` small and avoid
placing prompt-relevant text only in `details`; put model-visible tool output
in `content`.
## Prompt and model hooks
Use the phase-specific hooks for new plugins:

View File

@@ -44,6 +44,10 @@ function resolveMaxToolResultChars(opts?: { maxToolResultChars?: number }): numb
return Math.max(1, opts?.maxToolResultChars ?? DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS);
}
// `details` is runtime/UI metadata, not model-visible tool output. Keep the
// session JSONL useful for debugging without letting metadata blobs dominate
// disk, replay repair, transcript broadcasts, or future tooling that reads raw
// sessions. Model-visible text belongs in tool result `content`.
const MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES = 8_192;
const MAX_PERSISTED_DETAIL_STRING_CHARS = 2_000;
const MAX_PERSISTED_DETAIL_SESSION_COUNT = 10;
@@ -102,6 +106,9 @@ function buildPersistedDetailsFallback(
originalSize: BoundedJsonUtf8Bytes,
sanitizedBytes?: number,
): Record<string, unknown> {
// If even the structured summary is too large, keep only shape and stable
// status fields. This preserves "what happened?" without persisting the raw
// diagnostics payload that caused the cap to trip.
const fallback: Record<string, unknown> = {
persistedDetailsTruncated: true,
finalDetailsTruncated: true,
@@ -150,6 +157,8 @@ function sanitizeToolResultDetailsForPersistence(details: unknown): unknown {
if (details === undefined || details === null) {
return details;
}
// Measure with an early-exit walker so hostile or enormous details do not
// need to be fully stringified just to learn they exceed the persistence cap.
const originalSize = boundedJsonUtf8Bytes(details, MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES);
if (originalSize.complete && originalSize.bytes <= MAX_PERSISTED_TOOL_RESULT_DETAILS_BYTES) {
return details;