feat(dynamic-trim): rename trim-tool-result, add self-compact, drop list-tool-results

Align the openclaw side of the dynamic-* tool family with the new
Plexum design (decision #31, 2026-06-04 revision):

- trim-tool-result → dynamic-trim (same on-wire schema; same semantics)
- Drop list-tool-results entirely. Agents find the opaque tool_call_id
  by reading their own prior assistant message's toolCall block id
  instead of querying a separate "directory" tool. This removes a
  workflow-step prerequisite and matches how Anthropic-shaped APIs
  surface tool_use ids to the model anyway.
- On agent_end drain, ALSO self-compact the dynamic-trim's own
  tool_use.input: rewrite to {tool_call_id, _self_compacted: true}.
  Without this the bulky `replacement` text sits duplicated — once in
  the rewritten target tool_result, once in dynamic-trim's call input.
  Picks up the selfCallId from openclaw's execute(toolCallId, ...) first
  arg (was previously discarded as _id).

Cross-runtime contract: tool name, input schema, return shape, and
sentinel prefix ("[trimmed by self] ") match Plexum's dynamic-trim
in internal/dynmem/trim.go + internal/persistence/trim.go.

Sim e2e tested: dynamic-trim queues, agent_end drain rewrites both
the target tool_result content AND the trim call's tool_use input.
No takeover errors. trimmed_bytes positive on real workloads.
This commit is contained in:
h z
2026-06-04 07:47:30 +01:00
parent 209ab0d82e
commit 0b7f18253d
3 changed files with 167 additions and 253 deletions

View File

@@ -9,8 +9,7 @@ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
import { pcexec, pcexecSync } from './tools/pcexec.js';
import {
queueTrimToolResult,
listToolResults,
queueDynamicTrim,
drainTrimQueueForSession,
} from './tools/session-rewrite.js';
import {
@@ -192,76 +191,50 @@ function register(api: OpenClawPluginApi): void {
} as any;
});
// Register trim-tool-result — rewrite a past toolResult to free ctx tokens.
// Register dynamic-trim — agent-driven rewrite of a past tool_result
// block. On the next turn boundary (agent_end hook) the target tool
// result's content[].text is shrunk AND this dynamic-trim call's own
// tool_use input is also self-compacted to {tool_call_id, _self_compacted}
// so the bulky replacement text doesn't sit duplicated.
//
// Cross-runtime alignment with Plexum's dynamic-* family — same tool
// name, same input schema, same semantics; only the lifecycle hook name
// differs (openclaw agent_end / Plexum block mutation point).
api.registerTool((ctx) => {
const agentDir = ctx.agentDir;
const agentIdInner = ctx.agentId;
const sessionId = ctx.sessionId;
return {
name: 'trim-tool-result',
name: 'dynamic-trim',
description:
'Replace a past tool result in your own session with a shorter version (or empty sentinel). Use after extracting what you need from a noisy tool output to free ctx tokens. Irreversible — re-run the original tool if you mis-trim.',
'Rewrite a past tool_result block in your own session to a shorter version (or sentinel). ' +
'Use after extracting what you need from a noisy tool output to free ctx tokens for future turns. ' +
'The tool_call_id is the OPAQUE id (looks like "call_function_abc123_1") of the prior tool_use — ' +
'find it in your own prior assistant messages; topic/fact ids and small integers will NOT work here. ' +
'Irreversible — re-run the original tool if you mis-trim. ' +
'Side effect: this dynamic-trim call\'s own tool_use input is self-compacted on the same turn boundary.',
parameters: {
type: 'object',
properties: {
tool_call_id: {
type: 'string',
description: 'The id of the toolCall whose result you want to trim (from list-tool-results).',
description: 'The opaque id of the target tool_use (you find this by reading your own prior assistant messages — the id appears on each toolCall block).',
},
replacement: {
type: 'string',
description: 'Optional condensed text to keep. Omit to fully elide the result.',
description: 'Optional condensed text to keep in place of the original result. Omit / empty = full elide.',
},
},
required: ['tool_call_id'],
},
async execute(_id: string, params: any) {
const r = queueTrimToolResult(
async execute(selfCallId: string, params: any) {
const r = queueDynamicTrim(
{ agentDir, agentId: agentIdInner, sessionId },
{
tool_call_id: String(params.tool_call_id ?? ''),
replacement: params.replacement != null ? String(params.replacement) : undefined,
},
);
return { content: [{ type: 'text', text: JSON.stringify(r) }] };
},
} as any;
});
// Register list-tool-results — enumerate past toolResults to pick trim candidates.
api.registerTool((ctx) => {
const agentDir = ctx.agentDir;
const agentIdInner = ctx.agentId;
const sessionId = ctx.sessionId;
return {
name: 'list-tool-results',
description:
'List past toolResults in your own session ordered by size (largest first), with tool name, args summary, byte size, and turns-ago. Does NOT include the result content itself — use this to pick trim-tool-result targets without re-reading bulky outputs.',
parameters: {
type: 'object',
properties: {
min_bytes: { type: 'number', description: 'Only include results at least this many bytes.' },
older_than_turns: {
type: 'number',
description: 'Only include results from at least this many assistant turns ago (default 0).',
},
include_trimmed: {
type: 'boolean',
description: 'Include already-trimmed results (default false).',
},
limit: { type: 'number', description: 'Max entries to return (default 50).' },
},
},
async execute(_id: string, params: any) {
const r = await listToolResults(
{ agentDir, agentId: agentIdInner, sessionId },
{
min_bytes: typeof params.min_bytes === 'number' ? params.min_bytes : undefined,
older_than_turns:
typeof params.older_than_turns === 'number' ? params.older_than_turns : undefined,
include_trimmed: params.include_trimmed === true,
limit: typeof params.limit === 'number' ? params.limit : undefined,
},
selfCallId,
);
return { content: [{ type: 'text', text: JSON.stringify(r) }] };
},