chore(sync): mirror docs from openclaw/openclaw@674feda214
This commit is contained in:
parent
ca718133fb
commit
72ae355e0e
@ -1,5 +1,5 @@
|
||||
{
|
||||
"repository": "openclaw/openclaw",
|
||||
"sha": "78030d0d52bbe9aa191fa3ad422a44531e585f2f",
|
||||
"syncedAt": "2026-04-21T20:27:23.543Z"
|
||||
"sha": "674feda214f8151f7fd5a2aff1d10162993cf6c1",
|
||||
"syncedAt": "2026-04-21T20:30:40.370Z"
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
f135ddc1802b7f8b2d29bf495fd0ac1f497a89bab8164ca8c7c8f18efc010e6e plugin-sdk-api-baseline.json
|
||||
a47d06095ec5c3701a94888a11e89700d8a8511db46fa3122fb9407e160707b6 plugin-sdk-api-baseline.jsonl
|
||||
cfeee4630cb43ffc4d702f207d28d35962c6458aa8fd2b1671c35e0be158bb35 plugin-sdk-api-baseline.json
|
||||
af4fbf19861c6ec000b41ac5a3ded597700e45bb15f8b1d74bb2d1f550bd09b6 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@ -611,7 +611,7 @@ Teams markdown is more limited than Slack or Discord:
|
||||
|
||||
- Basic formatting works: **bold**, _italic_, `code`, links
|
||||
- Complex markdown (tables, nested lists) may not render correctly
|
||||
- Adaptive Cards are supported for polls and arbitrary card sends (see below)
|
||||
- Adaptive Cards are supported for polls and semantic presentation sends (see below)
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -783,11 +783,11 @@ OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API)
|
||||
- The gateway must stay online to record votes.
|
||||
- Polls do not auto-post result summaries yet (inspect the store file if needed).
|
||||
|
||||
## Adaptive Cards (arbitrary)
|
||||
## Presentation Cards
|
||||
|
||||
Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI.
|
||||
Send semantic presentation payloads to Teams users or conversations using the `message` tool or CLI. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract.
|
||||
|
||||
The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional.
|
||||
The `presentation` parameter accepts semantic blocks. When `presentation` is provided, the message text is optional.
|
||||
|
||||
**Agent tool:**
|
||||
|
||||
@ -796,10 +796,9 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid
|
||||
action: "send",
|
||||
channel: "msteams",
|
||||
target: "user:<id>",
|
||||
card: {
|
||||
type: "AdaptiveCard",
|
||||
version: "1.5",
|
||||
body: [{ type: "TextBlock", text: "Hello!" }],
|
||||
presentation: {
|
||||
title: "Hello",
|
||||
blocks: [{ type: "text", text: "Hello!" }],
|
||||
},
|
||||
}
|
||||
```
|
||||
@ -809,10 +808,10 @@ The `card` parameter accepts an Adaptive Card JSON object. When `card` is provid
|
||||
```bash
|
||||
openclaw message send --channel msteams \
|
||||
--target "conversation:19:abc...@thread.tacv2" \
|
||||
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}'
|
||||
--presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello!"}]}'
|
||||
```
|
||||
|
||||
See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below.
|
||||
For target format details, see [Target formats](#target-formats) below.
|
||||
|
||||
## Target formats
|
||||
|
||||
@ -837,9 +836,9 @@ openclaw message send --channel msteams --target "user:John Smith" --message "He
|
||||
# Send to a group chat or channel
|
||||
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello"
|
||||
|
||||
# Send an Adaptive Card to a conversation
|
||||
# Send a presentation card to a conversation
|
||||
openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \
|
||||
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}'
|
||||
--presentation '{"title":"Hello","blocks":[{"type":"text","text":"Hello"}]}'
|
||||
```
|
||||
|
||||
**Agent tool examples:**
|
||||
@ -858,10 +857,9 @@ openclaw message send --channel msteams --target "conversation:19:abc...@thread.
|
||||
action: "send",
|
||||
channel: "msteams",
|
||||
target: "conversation:19:abc...@thread.tacv2",
|
||||
card: {
|
||||
type: "AdaptiveCard",
|
||||
version: "1.5",
|
||||
body: [{ type: "TextBlock", text: "Hello" }],
|
||||
presentation: {
|
||||
title: "Hello",
|
||||
blocks: [{ type: "text", text: "Hello" }],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@ -803,7 +803,8 @@ openclaw message poll --channel telegram --target -1001234567890:topic:42 \
|
||||
|
||||
Telegram send also supports:
|
||||
|
||||
- `--buttons` for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it
|
||||
- `--presentation` with `buttons` blocks for inline keyboards when `channels.telegram.capabilities.inlineButtons` allows it
|
||||
- `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat
|
||||
- `--force-document` to send outbound images and GIFs as documents instead of compressed photo or animated-media uploads
|
||||
|
||||
Action gating:
|
||||
|
||||
@ -67,15 +67,13 @@ Name lookup:
|
||||
|
||||
- `send`
|
||||
- Channels: WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/Matrix/Microsoft Teams
|
||||
- Required: `--target`, plus `--message` or `--media`
|
||||
- Optional: `--media`, `--interactive`, `--buttons`, `--components`, `--card`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent`
|
||||
- Shared interactive payloads: `--interactive` sends a channel-native interactive JSON payload when supported
|
||||
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)
|
||||
- Required: `--target`, plus `--message`, `--media`, or `--presentation`
|
||||
- Optional: `--media`, `--presentation`, `--delivery`, `--pin`, `--reply-to`, `--thread-id`, `--gif-playback`, `--force-document`, `--silent`
|
||||
- Shared presentation payloads: `--presentation` sends semantic blocks (`text`, `context`, `divider`, `buttons`, `select`) that core renders through the selected channel's declared capabilities. See [Message Presentation](/plugins/message-presentation).
|
||||
- Generic delivery preferences: `--delivery` accepts delivery hints such as `{ "pin": true }`; `--pin` is shorthand for pinned delivery when the channel supports it.
|
||||
- Telegram only: `--force-document` (send images and GIFs as documents to avoid Telegram compression)
|
||||
- Telegram only: `--thread-id` (forum topic id)
|
||||
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
|
||||
- Discord only: `--components` JSON payload
|
||||
- Adaptive-card channels: `--card` JSON payload when supported
|
||||
- Telegram + Discord: `--silent`
|
||||
- WhatsApp only: `--gif-playback`
|
||||
|
||||
@ -208,22 +206,22 @@ openclaw message send --channel discord \
|
||||
--target channel:123 --message "hi" --reply-to 456
|
||||
```
|
||||
|
||||
Send a Discord message with components:
|
||||
Send a message with semantic buttons:
|
||||
|
||||
```
|
||||
openclaw message send --channel discord \
|
||||
--target channel:123 --message "Choose:" \
|
||||
--components '{"text":"Choose a path","blocks":[{"type":"actions","buttons":[{"label":"Approve","style":"success"},{"label":"Decline","style":"danger"}]}]}'
|
||||
--presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Approve","value":"approve","style":"success"},{"label":"Decline","value":"decline","style":"danger"}]}]}'
|
||||
```
|
||||
|
||||
See [Discord components](/channels/discord#interactive-components) for the full schema.
|
||||
Core renders the same `presentation` payload into Discord components, Slack blocks, Telegram inline buttons, Mattermost props, or Teams/Feishu cards depending on channel capability. See [Message Presentation](/plugins/message-presentation) for the full contract and fallback rules.
|
||||
|
||||
Send a shared interactive payload:
|
||||
Send a richer presentation payload:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel googlechat --target spaces/AAA... \
|
||||
--message "Choose:" \
|
||||
--interactive '{"text":"Choose a path","blocks":[{"type":"actions","buttons":[{"label":"Approve"},{"label":"Decline"}]}]}'
|
||||
--presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Choose a path"},{"type":"buttons","buttons":[{"label":"Approve","value":"approve"},{"label":"Decline","value":"decline"}]}]}'
|
||||
```
|
||||
|
||||
Create a Discord poll:
|
||||
@ -277,19 +275,19 @@ openclaw message react --channel signal \
|
||||
--emoji "✅" --target-author-uuid 123e4567-e89b-12d3-a456-426614174000
|
||||
```
|
||||
|
||||
Send Telegram inline buttons:
|
||||
Send Telegram inline buttons through generic presentation:
|
||||
|
||||
```
|
||||
openclaw message send --channel telegram --target @mychat --message "Choose:" \
|
||||
--buttons '[ [{"text":"Yes","callback_data":"cmd:yes"}], [{"text":"No","callback_data":"cmd:no"}] ]'
|
||||
--presentation '{"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"cmd:yes"},{"label":"No","value":"cmd:no"}]}]}'
|
||||
```
|
||||
|
||||
Send a Teams Adaptive Card:
|
||||
Send a Teams card through generic presentation:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel msteams \
|
||||
--target conversation:19:abc@thread.tacv2 \
|
||||
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Status update"}]}'
|
||||
--presentation '{"title":"Status update","blocks":[{"type":"text","text":"Build completed"}]}'
|
||||
```
|
||||
|
||||
Send a Telegram image as a document to avoid compression:
|
||||
|
||||
254
docs/plan/ui-channels.md
Normal file
254
docs/plan/ui-channels.md
Normal file
@ -0,0 +1,254 @@
|
||||
---
|
||||
title: Channel Presentation Refactor Plan
|
||||
summary: Decouple semantic message presentation from channel native UI renderers.
|
||||
read_when:
|
||||
- Refactoring channel message UI, interactive payloads, or native channel renderers
|
||||
- Changing message tool capabilities, delivery hints, or cross-context markers
|
||||
- Debugging Discord Carbon import fanout or channel plugin runtime laziness
|
||||
---
|
||||
|
||||
# Channel Presentation Refactor Plan
|
||||
|
||||
## Status
|
||||
|
||||
Implemented for the shared agent, CLI, plugin capability, and outbound delivery surfaces:
|
||||
|
||||
- `ReplyPayload.presentation` carries semantic message UI.
|
||||
- `ReplyPayload.delivery.pin` carries sent-message pin requests.
|
||||
- Shared message actions expose `presentation`, `delivery`, and `pin` instead of provider-native `components`, `blocks`, `buttons`, or `card`.
|
||||
- Core renders or auto-degrades presentation through plugin-declared outbound capabilities.
|
||||
- Discord, Slack, Telegram, Mattermost, MS Teams, and Feishu renderers consume the generic contract.
|
||||
- Discord channel control-plane code no longer imports Carbon-backed UI containers.
|
||||
|
||||
Canonical docs now live in [Message Presentation](/plugins/message-presentation).
|
||||
Keep this plan as historical implementation context; update the canonical guide
|
||||
for contract, renderer, or fallback behavior changes.
|
||||
|
||||
## Problem
|
||||
|
||||
Channel UI is currently split across several incompatible surfaces:
|
||||
|
||||
- Core owns a Discord-shaped cross-context renderer hook through `buildCrossContextComponents`.
|
||||
- Discord `channel.ts` can import native Carbon UI through `DiscordUiContainer`, which pulls runtime UI dependencies into the channel plugin control plane.
|
||||
- The agent and CLI expose native payload escape hatches such as Discord `components`, Slack `blocks`, Telegram or Mattermost `buttons`, and Teams or Feishu `card`.
|
||||
- `ReplyPayload.channelData` carries both transport hints and native UI envelopes.
|
||||
- The generic `interactive` model exists, but it is narrower than the richer layouts already used by Discord, Slack, Teams, Feishu, LINE, Telegram, and Mattermost.
|
||||
|
||||
This makes core aware of native UI shapes, weakens plugin runtime laziness, and gives agents too many provider-specific ways to express the same message intent.
|
||||
|
||||
## Goals
|
||||
|
||||
- Core decides the best semantic presentation for a message from declared capabilities.
|
||||
- Extensions declare capabilities and render semantic presentation into native transport payloads.
|
||||
- Web Control UI remains separate from chat native UI.
|
||||
- Native channel payloads are not exposed through the shared agent or CLI message surface.
|
||||
- Unsupported presentation features auto-degrade to the best text representation.
|
||||
- Delivery behavior such as pinning a sent message is generic delivery metadata, not presentation.
|
||||
|
||||
## Non Goals
|
||||
|
||||
- No backwards compatibility shim for `buildCrossContextComponents`.
|
||||
- No public native escape hatches for `components`, `blocks`, `buttons`, or `card`.
|
||||
- No core imports of channel-native UI libraries.
|
||||
- No provider-specific SDK seams for bundled channels.
|
||||
|
||||
## Target Model
|
||||
|
||||
Add a core-owned `presentation` field to `ReplyPayload`.
|
||||
|
||||
```ts
|
||||
type MessagePresentationTone = "neutral" | "info" | "success" | "warning" | "danger";
|
||||
|
||||
type MessagePresentation = {
|
||||
tone?: MessagePresentationTone;
|
||||
title?: string;
|
||||
blocks: MessagePresentationBlock[];
|
||||
};
|
||||
|
||||
type MessagePresentationBlock =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "context"; text: string }
|
||||
| { type: "divider" }
|
||||
| { type: "buttons"; buttons: MessagePresentationButton[] }
|
||||
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] };
|
||||
|
||||
type MessagePresentationButton = {
|
||||
label: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
style?: "primary" | "secondary" | "success" | "danger";
|
||||
};
|
||||
|
||||
type MessagePresentationOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
```
|
||||
|
||||
`interactive` becomes a subset of `presentation` during migration:
|
||||
|
||||
- `interactive` text block maps to `presentation.blocks[].type = "text"`.
|
||||
- `interactive` buttons block maps to `presentation.blocks[].type = "buttons"`.
|
||||
- `interactive` select block maps to `presentation.blocks[].type = "select"`.
|
||||
|
||||
The external agent and CLI schemas now use `presentation`; `interactive` remains an internal legacy parser/rendering helper for existing reply producers.
|
||||
|
||||
## Delivery Metadata
|
||||
|
||||
Add a core-owned `delivery` field for send behavior that is not UI.
|
||||
|
||||
```ts
|
||||
type ReplyPayloadDelivery = {
|
||||
pin?:
|
||||
| boolean
|
||||
| {
|
||||
enabled: boolean;
|
||||
notify?: boolean;
|
||||
required?: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
- `delivery.pin = true` means pin the first successfully delivered message.
|
||||
- `notify` defaults to `false`.
|
||||
- `required` defaults to `false`; unsupported channels or failed pinning auto-degrade by continuing delivery.
|
||||
- Manual `pin`, `unpin`, and `list-pins` message actions remain for existing messages.
|
||||
|
||||
Current Telegram ACP topic binding should move from `channelData.telegram.pin = true` to `delivery.pin = true`.
|
||||
|
||||
## Runtime Capability Contract
|
||||
|
||||
Add presentation and delivery render hooks to the runtime outbound adapter, not the control-plane channel plugin.
|
||||
|
||||
```ts
|
||||
type ChannelPresentationCapabilities = {
|
||||
supported: boolean;
|
||||
buttons?: boolean;
|
||||
selects?: boolean;
|
||||
context?: boolean;
|
||||
divider?: boolean;
|
||||
tones?: MessagePresentationTone[];
|
||||
};
|
||||
|
||||
type ChannelDeliveryCapabilities = {
|
||||
pinSentMessage?: boolean;
|
||||
};
|
||||
|
||||
type ChannelOutboundAdapter = {
|
||||
presentationCapabilities?: ChannelPresentationCapabilities;
|
||||
|
||||
renderPresentation?: (params: {
|
||||
payload: ReplyPayload;
|
||||
presentation: MessagePresentation;
|
||||
ctx: ChannelOutboundSendContext;
|
||||
}) => ReplyPayload | null;
|
||||
|
||||
deliveryCapabilities?: ChannelDeliveryCapabilities;
|
||||
|
||||
pinDeliveredMessage?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
to: string;
|
||||
threadId?: string | number | null;
|
||||
messageId: string;
|
||||
notify: boolean;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
```
|
||||
|
||||
Core behavior:
|
||||
|
||||
- Resolve target channel and runtime adapter.
|
||||
- Ask for presentation capabilities.
|
||||
- Degrade unsupported blocks before rendering.
|
||||
- Call `renderPresentation`.
|
||||
- If no renderer exists, convert presentation to text fallback.
|
||||
- After successful send, call `pinDeliveredMessage` when `delivery.pin` is requested and supported.
|
||||
|
||||
## Channel Mapping
|
||||
|
||||
Discord:
|
||||
|
||||
- Render `presentation` to components v2 and Carbon containers in runtime-only modules.
|
||||
- Keep accent color helpers in light modules.
|
||||
- Remove `DiscordUiContainer` imports from channel plugin control-plane code.
|
||||
|
||||
Slack:
|
||||
|
||||
- Render `presentation` to Block Kit.
|
||||
- Remove agent and CLI `blocks` input.
|
||||
|
||||
Telegram:
|
||||
|
||||
- Render text, context, and dividers as text.
|
||||
- Render actions and select as inline keyboards when configured and allowed for the target surface.
|
||||
- Use text fallback when inline buttons are disabled.
|
||||
- Move ACP topic pinning to `delivery.pin`.
|
||||
|
||||
Mattermost:
|
||||
|
||||
- Render actions as interactive buttons where configured.
|
||||
- Render other blocks as text fallback.
|
||||
|
||||
MS Teams:
|
||||
|
||||
- Render `presentation` to Adaptive Cards.
|
||||
- Keep manual pin/unpin/list-pins actions.
|
||||
- Optionally implement `pinDeliveredMessage` if Graph support is reliable for the target conversation.
|
||||
|
||||
Feishu:
|
||||
|
||||
- Render `presentation` to interactive cards.
|
||||
- Keep manual pin/unpin/list-pins actions.
|
||||
- Optionally implement `pinDeliveredMessage` for sent-message pinning if API behavior is reliable.
|
||||
|
||||
LINE:
|
||||
|
||||
- Render `presentation` to Flex or template messages where possible.
|
||||
- Fall back to text for unsupported blocks.
|
||||
- Remove LINE UI payloads from `channelData`.
|
||||
|
||||
Plain or limited channels:
|
||||
|
||||
- Convert presentation to text with conservative formatting.
|
||||
|
||||
## Refactor Steps
|
||||
|
||||
1. Reapply the Discord release fix that splits `ui-colors.ts` from Carbon-backed UI and removes `DiscordUiContainer` from `extensions/discord/src/channel.ts`.
|
||||
2. Add `presentation` and `delivery` to `ReplyPayload`, outbound payload normalization, delivery summaries, and hook payloads.
|
||||
3. Add `MessagePresentation` schema and parser helpers in a narrow SDK/runtime subpath.
|
||||
4. Replace message capabilities `buttons`, `cards`, `components`, and `blocks` with semantic presentation capabilities.
|
||||
5. Add runtime outbound adapter hooks for presentation render and delivery pinning.
|
||||
6. Replace cross-context component construction with `buildCrossContextPresentation`.
|
||||
7. Delete `src/infra/outbound/channel-adapters.ts` and remove `buildCrossContextComponents` from channel plugin types.
|
||||
8. Change `maybeApplyCrossContextMarker` to attach `presentation` instead of native params.
|
||||
9. Update plugin-dispatch send paths to consume only semantic presentation and delivery metadata.
|
||||
10. Remove agent and CLI native payload params: `components`, `blocks`, `buttons`, and `card`.
|
||||
11. Remove SDK helpers that create native message-tool schemas, replacing them with presentation schema helpers.
|
||||
12. Remove UI/native envelopes from `channelData`; keep only transport metadata until each remaining field is reviewed.
|
||||
13. Migrate Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, and LINE renderers.
|
||||
14. Update docs for message CLI, channel pages, plugin SDK, and capability cookbook.
|
||||
15. Run import fanout profiling for Discord and affected channel entrypoints.
|
||||
|
||||
Steps 1-11 and 13-14 are implemented in this refactor for the shared agent, CLI, plugin capability, and outbound adapter contracts. Step 12 remains a deeper internal cleanup pass for provider-private `channelData` transport envelopes. Step 15 remains follow-up validation if we want quantified import-fanout numbers beyond the type/test gate.
|
||||
|
||||
## Tests
|
||||
|
||||
Add or update:
|
||||
|
||||
- Presentation normalization tests.
|
||||
- Presentation auto-degrade tests for unsupported blocks.
|
||||
- Cross-context marker tests for plugin dispatch and core delivery paths.
|
||||
- Channel render matrix tests for Discord, Slack, Telegram, Mattermost, MS Teams, Feishu, LINE, and text fallback.
|
||||
- Message tool schema tests proving native fields are gone.
|
||||
- CLI tests proving native flags are gone.
|
||||
- Discord entrypoint import-laziness regression covering Carbon.
|
||||
- Delivery pin tests covering Telegram and generic fallback.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `delivery.pin` be implemented for Discord, Slack, MS Teams, and Feishu in the first pass, or only Telegram first?
|
||||
- Should `delivery` eventually absorb existing fields such as `replyToId`, `replyToCurrent`, `silent`, and `audioAsVoice`, or stay focused on post-send behaviors?
|
||||
- Should presentation support images or file references directly, or should media remain separate from UI layout for now?
|
||||
@ -1251,16 +1251,21 @@ Compatibility note:
|
||||
## Message tool schemas
|
||||
|
||||
Plugins should own channel-specific `describeMessageTool(...)` schema
|
||||
contributions. Keep provider-specific fields in the plugin, not in shared core.
|
||||
contributions for non-message primitives such as reactions, reads, and polls.
|
||||
Shared send presentation should use the generic `MessagePresentation` contract
|
||||
instead of provider-native button, component, block, or card fields.
|
||||
See [Message Presentation](/plugins/message-presentation) for the contract,
|
||||
fallback rules, provider mapping, and plugin author checklist.
|
||||
|
||||
For shared portable schema fragments, reuse the generic helpers exported through
|
||||
`openclaw/plugin-sdk/channel-actions`:
|
||||
Send-capable plugins declare what they can render through message capabilities:
|
||||
|
||||
- `createMessageToolButtonsSchema()` for button-grid style payloads
|
||||
- `createMessageToolCardSchema()` for structured card payloads
|
||||
- `presentation` for semantic presentation blocks (`text`, `context`, `divider`, `buttons`, `select`)
|
||||
- `delivery-pin` for pinned-delivery requests
|
||||
|
||||
If a schema shape only makes sense for one provider, define it in that plugin's
|
||||
own source instead of promoting it into the shared SDK.
|
||||
Core decides whether to render the presentation natively or degrade it to text.
|
||||
Do not expose provider-native UI escape hatches from the generic message tool.
|
||||
Deprecated SDK helpers for legacy native schemas remain exported for existing
|
||||
third-party plugins, but new plugins should not use them.
|
||||
|
||||
## Channel target resolution
|
||||
|
||||
|
||||
338
docs/plugins/message-presentation.md
Normal file
338
docs/plugins/message-presentation.md
Normal file
@ -0,0 +1,338 @@
|
||||
---
|
||||
title: "Message Presentation"
|
||||
summary: "Semantic message cards, buttons, selects, fallback text, and delivery hints for channel plugins"
|
||||
read_when:
|
||||
- Adding or modifying message card, button, or select rendering
|
||||
- Building a channel plugin that supports rich outbound messages
|
||||
- Changing message tool presentation or delivery capabilities
|
||||
- Debugging provider-specific card/block/component rendering regressions
|
||||
---
|
||||
|
||||
# Message Presentation
|
||||
|
||||
Message presentation is OpenClaw's shared contract for rich outbound chat UI.
|
||||
It lets agents, CLI commands, approval flows, and plugins describe the message
|
||||
intent once, while each channel plugin renders the best native shape it can.
|
||||
|
||||
Use presentation for portable message UI:
|
||||
|
||||
- text sections
|
||||
- small context/footer text
|
||||
- dividers
|
||||
- buttons
|
||||
- select menus
|
||||
- card title and tone
|
||||
|
||||
Do not add new provider-native fields such as Discord `components`, Slack
|
||||
`blocks`, Telegram `buttons`, Teams `card`, or Feishu `card` to the shared
|
||||
message tool. Those are renderer outputs owned by the channel plugin.
|
||||
|
||||
## Contract
|
||||
|
||||
Plugin authors import the public contract from:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
MessagePresentation,
|
||||
ReplyPayloadDelivery,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
```
|
||||
|
||||
Shape:
|
||||
|
||||
```ts
|
||||
type MessagePresentation = {
|
||||
title?: string;
|
||||
tone?: "neutral" | "info" | "success" | "warning" | "danger";
|
||||
blocks: MessagePresentationBlock[];
|
||||
};
|
||||
|
||||
type MessagePresentationBlock =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "context"; text: string }
|
||||
| { type: "divider" }
|
||||
| { type: "buttons"; buttons: MessagePresentationButton[] }
|
||||
| { type: "select"; placeholder?: string; options: MessagePresentationOption[] };
|
||||
|
||||
type MessagePresentationButton = {
|
||||
label: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
style?: "primary" | "secondary" | "success" | "danger";
|
||||
};
|
||||
|
||||
type MessagePresentationOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type ReplyPayloadDelivery = {
|
||||
pin?:
|
||||
| boolean
|
||||
| {
|
||||
enabled: boolean;
|
||||
notify?: boolean;
|
||||
required?: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Button semantics:
|
||||
|
||||
- `value` is an application action value routed back through the channel's
|
||||
existing interaction path when the channel supports clickable controls.
|
||||
- `url` is a link button. It can exist without `value`.
|
||||
- `label` is required and is also used in text fallback.
|
||||
- `style` is advisory. Renderers should map unsupported styles to a safe
|
||||
default, not fail the send.
|
||||
|
||||
Select semantics:
|
||||
|
||||
- `options[].value` is the selected application value.
|
||||
- `placeholder` is advisory and may be ignored by channels without native
|
||||
select support.
|
||||
- If a channel does not support selects, fallback text lists the labels.
|
||||
|
||||
## Producer Examples
|
||||
|
||||
Simple card:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Deploy approval",
|
||||
"tone": "warning",
|
||||
"blocks": [
|
||||
{ "type": "text", "text": "Canary is ready to promote." },
|
||||
{ "type": "context", "text": "Build 1234, staging passed." },
|
||||
{
|
||||
"type": "buttons",
|
||||
"buttons": [
|
||||
{ "label": "Approve", "value": "deploy:approve", "style": "success" },
|
||||
{ "label": "Decline", "value": "deploy:decline", "style": "danger" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
URL-only link button:
|
||||
|
||||
```json
|
||||
{
|
||||
"blocks": [
|
||||
{ "type": "text", "text": "Release notes are ready." },
|
||||
{
|
||||
"type": "buttons",
|
||||
"buttons": [{ "label": "Open notes", "url": "https://example.com/release" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Select menu:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Choose environment",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "select",
|
||||
"placeholder": "Environment",
|
||||
"options": [
|
||||
{ "label": "Canary", "value": "env:canary" },
|
||||
{ "label": "Production", "value": "env:prod" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
CLI send:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel slack \
|
||||
--target channel:C123 \
|
||||
--message "Deploy approval" \
|
||||
--presentation '{"title":"Deploy approval","tone":"warning","blocks":[{"type":"text","text":"Canary is ready."},{"type":"buttons","buttons":[{"label":"Approve","value":"deploy:approve","style":"success"},{"label":"Decline","value":"deploy:decline","style":"danger"}]}]}'
|
||||
```
|
||||
|
||||
Pinned delivery:
|
||||
|
||||
```bash
|
||||
openclaw message send --channel telegram \
|
||||
--target -1001234567890 \
|
||||
--message "Topic opened" \
|
||||
--pin
|
||||
```
|
||||
|
||||
Pinned delivery with explicit JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"pin": {
|
||||
"enabled": true,
|
||||
"notify": true,
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Renderer Contract
|
||||
|
||||
Channel plugins declare render support on their outbound adapter:
|
||||
|
||||
```ts
|
||||
const adapter: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
presentationCapabilities: {
|
||||
supported: true,
|
||||
buttons: true,
|
||||
selects: true,
|
||||
context: true,
|
||||
divider: true,
|
||||
},
|
||||
deliveryCapabilities: {
|
||||
pin: true,
|
||||
},
|
||||
renderPresentation({ payload, presentation, ctx }) {
|
||||
return renderNativePayload(payload, presentation, ctx);
|
||||
},
|
||||
async pinDeliveredMessage({ target, messageId, pin }) {
|
||||
await pinNativeMessage(target, messageId, { notify: pin.notify === true });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Capability fields are intentionally simple booleans. They describe what the
|
||||
renderer can make interactive, not every native platform limit. Renderers still
|
||||
own platform-specific limits such as maximum button count, block count, and
|
||||
card size.
|
||||
|
||||
## Core Render Flow
|
||||
|
||||
When a `ReplyPayload` or message action includes `presentation`, core:
|
||||
|
||||
1. Normalizes the presentation payload.
|
||||
2. Resolves the target channel's outbound adapter.
|
||||
3. Reads `presentationCapabilities`.
|
||||
4. Calls `renderPresentation` when the adapter can render the payload.
|
||||
5. Falls back to conservative text when the adapter is absent or cannot render.
|
||||
6. Sends the resulting payload through the normal channel delivery path.
|
||||
7. Applies delivery metadata such as `delivery.pin` after the first successful
|
||||
sent message.
|
||||
|
||||
Core owns fallback behavior so producers can stay channel-agnostic. Channel
|
||||
plugins own native rendering and interaction handling.
|
||||
|
||||
## Degradation Rules
|
||||
|
||||
Presentation must be safe to send on limited channels.
|
||||
|
||||
Fallback text includes:
|
||||
|
||||
- `title` as the first line
|
||||
- `text` blocks as normal paragraphs
|
||||
- `context` blocks as compact context lines
|
||||
- `divider` blocks as a visual separator
|
||||
- button labels, including URLs for link buttons
|
||||
- select option labels
|
||||
|
||||
Unsupported native controls should degrade rather than fail the whole send.
|
||||
Examples:
|
||||
|
||||
- Telegram with inline buttons disabled sends text fallback.
|
||||
- A channel without select support lists select options as text.
|
||||
- A URL-only button becomes either a native link button or a fallback URL line.
|
||||
- Optional pin failures do not fail the delivered message.
|
||||
|
||||
The main exception is `delivery.pin.required: true`; if pinning is requested as
|
||||
required and the channel cannot pin the sent message, delivery reports failure.
|
||||
|
||||
## Provider Mapping
|
||||
|
||||
Current bundled renderers:
|
||||
|
||||
| Channel | Native render target | Notes |
|
||||
| --------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Discord | Components and component containers | Preserves legacy `channelData.discord.components` for existing provider-native payload producers, but new shared sends should use `presentation`. |
|
||||
| Slack | Block Kit | Preserves legacy `channelData.slack.blocks` for existing provider-native payload producers, but new shared sends should use `presentation`. |
|
||||
| Telegram | Text plus inline keyboards | Buttons/selects require inline button capability for the target surface; otherwise text fallback is used. |
|
||||
| Mattermost | Text plus interactive props | Other blocks degrade to text. |
|
||||
| Microsoft Teams | Adaptive Cards | Plain `message` text is included with the card when both are provided. |
|
||||
| Feishu | Interactive cards | Card header can use `title`; body avoids duplicating that title. |
|
||||
| Plain channels | Text fallback | Channels without a renderer still get readable output. |
|
||||
|
||||
Provider-native payload compatibility is a transition affordance for existing
|
||||
reply producers. It is not a reason to add new shared native fields.
|
||||
|
||||
## Presentation vs InteractiveReply
|
||||
|
||||
`InteractiveReply` is the older internal subset used by approval and interaction
|
||||
helpers. It supports:
|
||||
|
||||
- text
|
||||
- buttons
|
||||
- selects
|
||||
|
||||
`MessagePresentation` is the canonical shared send contract. It adds:
|
||||
|
||||
- title
|
||||
- tone
|
||||
- context
|
||||
- divider
|
||||
- URL-only buttons
|
||||
- generic delivery metadata through `ReplyPayload.delivery`
|
||||
|
||||
Use helpers from `openclaw/plugin-sdk/interactive-runtime` when bridging older
|
||||
code:
|
||||
|
||||
```ts
|
||||
import {
|
||||
interactiveReplyToPresentation,
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
renderMessagePresentationFallbackText,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
```
|
||||
|
||||
New code should accept or produce `MessagePresentation` directly.
|
||||
|
||||
## Delivery Pin
|
||||
|
||||
Pinning is delivery behavior, not presentation. Use `delivery.pin` instead of
|
||||
provider-native fields such as `channelData.telegram.pin`.
|
||||
|
||||
Semantics:
|
||||
|
||||
- `pin: true` pins the first successfully delivered message.
|
||||
- `pin.notify` defaults to `false`.
|
||||
- `pin.required` defaults to `false`.
|
||||
- Optional pin failures degrade and leave the sent message intact.
|
||||
- Required pin failures fail delivery.
|
||||
- Chunked messages pin the first delivered chunk, not the tail chunk.
|
||||
|
||||
Manual `pin`, `unpin`, and `pins` message actions still exist for existing
|
||||
messages where the provider supports those operations.
|
||||
|
||||
## Plugin Author Checklist
|
||||
|
||||
- Declare `presentation` from `describeMessageTool(...)` when the channel can
|
||||
render or safely degrade semantic presentation.
|
||||
- Add `presentationCapabilities` to the runtime outbound adapter.
|
||||
- Implement `renderPresentation` in runtime code, not control-plane plugin
|
||||
setup code.
|
||||
- Keep native UI libraries out of hot setup/catalog paths.
|
||||
- Preserve platform limits in the renderer and tests.
|
||||
- Add fallback tests for unsupported buttons, selects, URL buttons, title/text
|
||||
duplication, and mixed `message` plus `presentation` sends.
|
||||
- Add delivery pin support through `deliveryCapabilities.pin` and
|
||||
`pinDeliveredMessage` only when the provider can pin the sent message id.
|
||||
- Do not expose new provider-native card/block/component/button fields through
|
||||
the shared message action schema.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Message CLI](/cli/message)
|
||||
- [Plugin SDK Overview](/plugins/sdk-overview)
|
||||
- [Plugin Architecture](/plugins/architecture#message-tool-schemas)
|
||||
- [Channel Presentation Refactor Plan](/plan/ui-channels)
|
||||
@ -109,13 +109,13 @@ explicitly promotes one as public.
|
||||
| `plugin-sdk/allowlist-config-edit` | Allowlist config edit/read helpers |
|
||||
| `plugin-sdk/group-access` | Shared group-access decision helpers |
|
||||
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
|
||||
| `plugin-sdk/interactive-runtime` | Interactive reply payload normalization/reduction helpers |
|
||||
| `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers. See [Message Presentation](/plugins/message-presentation) |
|
||||
| `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
|
||||
| `plugin-sdk/channel-mention-gating` | Narrow mention-policy helpers without the broader inbound runtime surface |
|
||||
| `plugin-sdk/channel-location` | Channel location context and formatting helpers |
|
||||
| `plugin-sdk/channel-logging` | Channel logging helpers for inbound drops and typing/ack failures |
|
||||
| `plugin-sdk/channel-send-result` | Reply result types |
|
||||
| `plugin-sdk/channel-actions` | `createMessageToolButtonsSchema`, `createMessageToolCardSchema` |
|
||||
| `plugin-sdk/channel-actions` | Channel message-action helpers, plus deprecated native schema helpers kept for plugin compatibility |
|
||||
| `plugin-sdk/channel-targets` | Target parsing/matching helpers |
|
||||
| `plugin-sdk/channel-contract` | Channel contract types |
|
||||
| `plugin-sdk/channel-feedback` | Feedback/reaction wiring |
|
||||
|
||||
731
docs/plugins/skill-workshop.md
Normal file
731
docs/plugins/skill-workshop.md
Normal file
@ -0,0 +1,731 @@
|
||||
---
|
||||
title: "Skill Workshop Plugin"
|
||||
summary: "Experimental capture of reusable procedures as workspace skills with review, approval, quarantine, and hot skill refresh"
|
||||
read_when:
|
||||
- You want agents to turn corrections or reusable procedures into workspace skills
|
||||
- You are configuring procedural skill memory
|
||||
- You are debugging skill_workshop tool behavior
|
||||
- You are deciding whether to enable automatic skill creation
|
||||
---
|
||||
|
||||
# Skill Workshop Plugin
|
||||
|
||||
Skill Workshop is **experimental**. It is disabled by default, its capture
|
||||
heuristics and reviewer prompts may change between releases, and automatic
|
||||
writes should be used only in trusted workspaces after reviewing pending-mode
|
||||
output first.
|
||||
|
||||
Skill Workshop is procedural memory for workspace skills. It lets an agent turn
|
||||
reusable workflows, user corrections, hard-won fixes, and recurring pitfalls
|
||||
into `SKILL.md` files under:
|
||||
|
||||
```text
|
||||
<workspace>/skills/<skill-name>/SKILL.md
|
||||
```
|
||||
|
||||
This is different from long-term memory:
|
||||
|
||||
- **Memory** stores facts, preferences, entities, and past context.
|
||||
- **Skills** store reusable procedures the agent should follow on future tasks.
|
||||
- **Skill Workshop** is the bridge from a useful turn to a durable workspace
|
||||
skill, with safety checks and optional approval.
|
||||
|
||||
Skill Workshop is useful when the agent learns a procedure such as:
|
||||
|
||||
- how to validate externally sourced animated GIF assets
|
||||
- how to replace screenshot assets and verify dimensions
|
||||
- how to run a repo-specific QA scenario
|
||||
- how to debug a recurring provider failure
|
||||
- how to repair a stale local workflow note
|
||||
|
||||
It is not intended for:
|
||||
|
||||
- facts like “the user likes blue”
|
||||
- broad autobiographical memory
|
||||
- raw transcript archiving
|
||||
- secrets, credentials, or hidden prompt text
|
||||
- one-off instructions that will not repeat
|
||||
|
||||
## Default State
|
||||
|
||||
The bundled plugin is **experimental** and **disabled by default** unless it is
|
||||
explicitly enabled in `plugins.entries.skill-workshop`.
|
||||
|
||||
The plugin manifest does not set `enabledByDefault: true`. The `enabled: true`
|
||||
default inside the plugin config schema applies only after the plugin entry has
|
||||
already been selected and loaded.
|
||||
|
||||
Experimental means:
|
||||
|
||||
- the plugin is supported enough for opt-in testing and dogfooding
|
||||
- proposal storage, reviewer thresholds, and capture heuristics can evolve
|
||||
- pending approval is the recommended starting mode
|
||||
- auto apply is for trusted personal/workspace setups, not shared or hostile
|
||||
input-heavy environments
|
||||
|
||||
## Enable
|
||||
|
||||
Minimal safe config:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"skill-workshop": {
|
||||
enabled: true,
|
||||
config: {
|
||||
autoCapture: true,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "hybrid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
With this config:
|
||||
|
||||
- the `skill_workshop` tool is available
|
||||
- explicit reusable corrections are queued as pending proposals
|
||||
- threshold-based reviewer passes can propose skill updates
|
||||
- no skill file is written until a pending proposal is applied
|
||||
|
||||
Use automatic writes only in trusted workspaces:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
"skill-workshop": {
|
||||
enabled: true,
|
||||
config: {
|
||||
autoCapture: true,
|
||||
approvalPolicy: "auto",
|
||||
reviewMode: "hybrid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`approvalPolicy: "auto"` still uses the same scanner and quarantine path. It
|
||||
does not apply proposals with critical findings.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Key | Default | Range / values | Meaning |
|
||||
| -------------------- | ----------- | ------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `enabled` | `true` | boolean | Enables the plugin after the plugin entry is loaded. |
|
||||
| `autoCapture` | `true` | boolean | Enables post-turn capture/review on successful agent turns. |
|
||||
| `approvalPolicy` | `"pending"` | `"pending"`, `"auto"` | Queue proposals or write safe proposals automatically. |
|
||||
| `reviewMode` | `"hybrid"` | `"off"`, `"heuristic"`, `"llm"`, `"hybrid"` | Chooses explicit correction capture, LLM reviewer, both, or neither. |
|
||||
| `reviewInterval` | `15` | `1..200` | Run reviewer after this many successful turns. |
|
||||
| `reviewMinToolCalls` | `8` | `1..500` | Run reviewer after this many observed tool calls. |
|
||||
| `reviewTimeoutMs` | `45000` | `5000..180000` | Timeout for the embedded reviewer run. |
|
||||
| `maxPending` | `50` | `1..200` | Max pending/quarantined proposals kept per workspace. |
|
||||
| `maxSkillBytes` | `40000` | `1024..200000` | Max generated skill/support file size. |
|
||||
|
||||
Recommended profiles:
|
||||
|
||||
```json5
|
||||
// Conservative: explicit tool use only, no automatic capture.
|
||||
{
|
||||
autoCapture: false,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "off",
|
||||
}
|
||||
```
|
||||
|
||||
```json5
|
||||
// Review-first: capture automatically, but require approval.
|
||||
{
|
||||
autoCapture: true,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "hybrid",
|
||||
}
|
||||
```
|
||||
|
||||
```json5
|
||||
// Trusted automation: write safe proposals immediately.
|
||||
{
|
||||
autoCapture: true,
|
||||
approvalPolicy: "auto",
|
||||
reviewMode: "hybrid",
|
||||
}
|
||||
```
|
||||
|
||||
```json5
|
||||
// Low-cost: no reviewer LLM call, only explicit correction phrases.
|
||||
{
|
||||
autoCapture: true,
|
||||
approvalPolicy: "pending",
|
||||
reviewMode: "heuristic",
|
||||
}
|
||||
```
|
||||
|
||||
## Capture Paths
|
||||
|
||||
Skill Workshop has three capture paths.
|
||||
|
||||
### Tool Suggestions
|
||||
|
||||
The model can call `skill_workshop` directly when it sees a reusable procedure
|
||||
or when the user asks it to save/update a skill.
|
||||
|
||||
This is the most explicit path and works even with `autoCapture: false`.
|
||||
|
||||
### Heuristic Capture
|
||||
|
||||
When `autoCapture` is enabled and `reviewMode` is `heuristic` or `hybrid`, the
|
||||
plugin scans successful turns for explicit user correction phrases:
|
||||
|
||||
- `next time`
|
||||
- `from now on`
|
||||
- `remember to`
|
||||
- `make sure to`
|
||||
- `always ... use/check/verify/record/save/prefer`
|
||||
- `prefer ... when/for/instead/use`
|
||||
- `when asked`
|
||||
|
||||
The heuristic creates a proposal from the latest matching user instruction. It
|
||||
uses topic hints to choose skill names for common workflows:
|
||||
|
||||
- animated GIF tasks -> `animated-gif-workflow`
|
||||
- screenshot or asset tasks -> `screenshot-asset-workflow`
|
||||
- QA or scenario tasks -> `qa-scenario-workflow`
|
||||
- GitHub PR tasks -> `github-pr-workflow`
|
||||
- fallback -> `learned-workflows`
|
||||
|
||||
Heuristic capture is intentionally narrow. It is for clear corrections and
|
||||
repeatable process notes, not for general transcript summarization.
|
||||
|
||||
### LLM Reviewer
|
||||
|
||||
When `autoCapture` is enabled and `reviewMode` is `llm` or `hybrid`, the plugin
|
||||
runs a compact embedded reviewer after thresholds are reached.
|
||||
|
||||
The reviewer receives:
|
||||
|
||||
- the recent transcript text, capped to the last 12,000 characters
|
||||
- up to 12 existing workspace skills
|
||||
- up to 2,000 characters from each existing skill
|
||||
- JSON-only instructions
|
||||
|
||||
The reviewer has no tools:
|
||||
|
||||
- `disableTools: true`
|
||||
- `toolsAllow: []`
|
||||
- `disableMessageTool: true`
|
||||
|
||||
It can return:
|
||||
|
||||
```json
|
||||
{ "action": "none" }
|
||||
```
|
||||
|
||||
or one skill proposal:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "create",
|
||||
"skillName": "media-asset-qa",
|
||||
"title": "Media Asset QA",
|
||||
"reason": "Reusable animated media acceptance workflow",
|
||||
"description": "Validate externally sourced animated media before product use.",
|
||||
"body": "## Workflow\n\n- Verify true animation.\n- Record attribution.\n- Store a local approved copy.\n- Verify in product UI before final reply."
|
||||
}
|
||||
```
|
||||
|
||||
It can also append to an existing skill:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "append",
|
||||
"skillName": "qa-scenario-workflow",
|
||||
"title": "QA Scenario Workflow",
|
||||
"reason": "Animated media QA needs reusable checks",
|
||||
"description": "QA scenario workflow.",
|
||||
"section": "Workflow",
|
||||
"body": "- For animated GIF tasks, verify frame count and attribution before passing."
|
||||
}
|
||||
```
|
||||
|
||||
Or replace exact text in an existing skill:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "replace",
|
||||
"skillName": "screenshot-asset-workflow",
|
||||
"title": "Screenshot Asset Workflow",
|
||||
"reason": "Old validation missed image optimization",
|
||||
"oldText": "- Replace the screenshot asset.",
|
||||
"newText": "- Replace the screenshot asset, preserve dimensions, optimize the PNG, and run the relevant validation gate."
|
||||
}
|
||||
```
|
||||
|
||||
Prefer `append` or `replace` when a relevant skill already exists. Use `create`
|
||||
only when no existing skill fits.
|
||||
|
||||
## Proposal Lifecycle
|
||||
|
||||
Every generated update becomes a proposal with:
|
||||
|
||||
- `id`
|
||||
- `createdAt`
|
||||
- `updatedAt`
|
||||
- `workspaceDir`
|
||||
- optional `agentId`
|
||||
- optional `sessionId`
|
||||
- `skillName`
|
||||
- `title`
|
||||
- `reason`
|
||||
- `source`: `tool`, `agent_end`, or `reviewer`
|
||||
- `status`
|
||||
- `change`
|
||||
- optional `scanFindings`
|
||||
- optional `quarantineReason`
|
||||
|
||||
Proposal statuses:
|
||||
|
||||
- `pending` - waiting for approval
|
||||
- `applied` - written to `<workspace>/skills`
|
||||
- `rejected` - rejected by operator/model
|
||||
- `quarantined` - blocked by critical scanner findings
|
||||
|
||||
State is stored per workspace under the Gateway state directory:
|
||||
|
||||
```text
|
||||
<stateDir>/skill-workshop/<workspace-hash>.json
|
||||
```
|
||||
|
||||
Pending and quarantined proposals are deduplicated by skill name and change
|
||||
payload. The store keeps the newest pending/quarantined proposals up to
|
||||
`maxPending`.
|
||||
|
||||
## Tool Reference
|
||||
|
||||
The plugin registers one agent tool:
|
||||
|
||||
```text
|
||||
skill_workshop
|
||||
```
|
||||
|
||||
### `status`
|
||||
|
||||
Count proposals by state for the active workspace.
|
||||
|
||||
```json
|
||||
{ "action": "status" }
|
||||
```
|
||||
|
||||
Result shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaceDir": "/path/to/workspace",
|
||||
"pending": 1,
|
||||
"quarantined": 0,
|
||||
"applied": 3,
|
||||
"rejected": 0
|
||||
}
|
||||
```
|
||||
|
||||
### `list_pending`
|
||||
|
||||
List pending proposals.
|
||||
|
||||
```json
|
||||
{ "action": "list_pending" }
|
||||
```
|
||||
|
||||
To list another status:
|
||||
|
||||
```json
|
||||
{ "action": "list_pending", "status": "applied" }
|
||||
```
|
||||
|
||||
Valid `status` values:
|
||||
|
||||
- `pending`
|
||||
- `applied`
|
||||
- `rejected`
|
||||
- `quarantined`
|
||||
|
||||
### `list_quarantine`
|
||||
|
||||
List quarantined proposals.
|
||||
|
||||
```json
|
||||
{ "action": "list_quarantine" }
|
||||
```
|
||||
|
||||
Use this when automatic capture appears to do nothing and the logs mention
|
||||
`skill-workshop: quarantined <skill>`.
|
||||
|
||||
### `inspect`
|
||||
|
||||
Fetch a proposal by id.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "inspect",
|
||||
"id": "proposal-id"
|
||||
}
|
||||
```
|
||||
|
||||
### `suggest`
|
||||
|
||||
Create a proposal. With `approvalPolicy: "pending"`, this queues by default.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"skillName": "animated-gif-workflow",
|
||||
"title": "Animated GIF Workflow",
|
||||
"reason": "User established reusable GIF validation rules.",
|
||||
"description": "Validate animated GIF assets before using them.",
|
||||
"body": "## Workflow\n\n- Verify the URL resolves to image/gif.\n- Confirm it has multiple frames.\n- Record attribution and license.\n- Avoid hotlinking when a local asset is needed."
|
||||
}
|
||||
```
|
||||
|
||||
Force a safe write:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"apply": true,
|
||||
"skillName": "animated-gif-workflow",
|
||||
"description": "Validate animated GIF assets before using them.",
|
||||
"body": "## Workflow\n\n- Verify true animation.\n- Record attribution."
|
||||
}
|
||||
```
|
||||
|
||||
Force pending even in `approvalPolicy: "auto"`:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"apply": false,
|
||||
"skillName": "screenshot-asset-workflow",
|
||||
"description": "Screenshot replacement workflow.",
|
||||
"body": "## Workflow\n\n- Verify dimensions.\n- Optimize the PNG.\n- Run the relevant gate."
|
||||
}
|
||||
```
|
||||
|
||||
Append to a section:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"skillName": "qa-scenario-workflow",
|
||||
"section": "Workflow",
|
||||
"description": "QA scenario workflow.",
|
||||
"body": "- For media QA, verify generated assets render and pass final assertions."
|
||||
}
|
||||
```
|
||||
|
||||
Replace exact text:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "suggest",
|
||||
"skillName": "github-pr-workflow",
|
||||
"oldText": "- Check the PR.",
|
||||
"newText": "- Check unresolved review threads, CI status, linked issues, and changed files before deciding."
|
||||
}
|
||||
```
|
||||
|
||||
### `apply`
|
||||
|
||||
Apply a pending proposal.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "apply",
|
||||
"id": "proposal-id"
|
||||
}
|
||||
```
|
||||
|
||||
`apply` refuses quarantined proposals:
|
||||
|
||||
```text
|
||||
quarantined proposal cannot be applied
|
||||
```
|
||||
|
||||
### `reject`
|
||||
|
||||
Mark a proposal rejected.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "reject",
|
||||
"id": "proposal-id"
|
||||
}
|
||||
```
|
||||
|
||||
### `write_support_file`
|
||||
|
||||
Write a supporting file inside an existing or proposed skill directory.
|
||||
|
||||
Allowed top-level support directories:
|
||||
|
||||
- `references/`
|
||||
- `templates/`
|
||||
- `scripts/`
|
||||
- `assets/`
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "write_support_file",
|
||||
"skillName": "release-workflow",
|
||||
"relativePath": "references/checklist.md",
|
||||
"body": "# Release Checklist\n\n- Run release docs.\n- Verify changelog.\n"
|
||||
}
|
||||
```
|
||||
|
||||
Support files are workspace-scoped, path-checked, byte-limited by
|
||||
`maxSkillBytes`, scanned, and written atomically.
|
||||
|
||||
## Skill Writes
|
||||
|
||||
Skill Workshop writes only under:
|
||||
|
||||
```text
|
||||
<workspace>/skills/<normalized-skill-name>/
|
||||
```
|
||||
|
||||
Skill names are normalized:
|
||||
|
||||
- lowercased
|
||||
- non `[a-z0-9_-]` runs become `-`
|
||||
- leading/trailing non-alphanumerics are removed
|
||||
- max length is 80 characters
|
||||
- final name must match `[a-z0-9][a-z0-9_-]{1,79}`
|
||||
|
||||
For `create`:
|
||||
|
||||
- if the skill does not exist, Skill Workshop writes a new `SKILL.md`
|
||||
- if it already exists, Skill Workshop appends the body to `## Workflow`
|
||||
|
||||
For `append`:
|
||||
|
||||
- if the skill exists, Skill Workshop appends to the requested section
|
||||
- if it does not exist, Skill Workshop creates a minimal skill then appends
|
||||
|
||||
For `replace`:
|
||||
|
||||
- the skill must already exist
|
||||
- `oldText` must be present exactly
|
||||
- only the first exact match is replaced
|
||||
|
||||
All writes are atomic and refresh the in-memory skills snapshot immediately, so
|
||||
the new or updated skill can become visible without a Gateway restart.
|
||||
|
||||
## Safety Model
|
||||
|
||||
Skill Workshop has a safety scanner on generated `SKILL.md` content and support
|
||||
files.
|
||||
|
||||
Critical findings quarantine proposals:
|
||||
|
||||
| Rule id | Blocks content that... |
|
||||
| -------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `prompt-injection-ignore-instructions` | tells the agent to ignore prior/higher instructions |
|
||||
| `prompt-injection-system` | references system prompts, developer messages, or hidden instructions |
|
||||
| `prompt-injection-tool` | encourages bypassing tool permission/approval |
|
||||
| `shell-pipe-to-shell` | includes `curl`/`wget` piped into `sh`, `bash`, or `zsh` |
|
||||
| `secret-exfiltration` | appears to send env/process env data over the network |
|
||||
|
||||
Warn findings are retained but do not block by themselves:
|
||||
|
||||
| Rule id | Warns on... |
|
||||
| -------------------- | -------------------------------- |
|
||||
| `destructive-delete` | broad `rm -rf` style commands |
|
||||
| `unsafe-permissions` | `chmod 777` style permission use |
|
||||
|
||||
Quarantined proposals:
|
||||
|
||||
- keep `scanFindings`
|
||||
- keep `quarantineReason`
|
||||
- appear in `list_quarantine`
|
||||
- cannot be applied through `apply`
|
||||
|
||||
To recover from a quarantined proposal, create a new safe proposal with the
|
||||
unsafe content removed. Do not edit the store JSON by hand.
|
||||
|
||||
## Prompt Guidance
|
||||
|
||||
When enabled, Skill Workshop injects a short prompt section that tells the agent
|
||||
to use `skill_workshop` for durable procedural memory.
|
||||
|
||||
The guidance emphasizes:
|
||||
|
||||
- procedures, not facts/preferences
|
||||
- user corrections
|
||||
- non-obvious successful procedures
|
||||
- recurring pitfalls
|
||||
- stale/thin/wrong skill repair through append/replace
|
||||
- saving reusable procedure after long tool loops or hard fixes
|
||||
- short imperative skill text
|
||||
- no transcript dumps
|
||||
|
||||
The write mode text changes with `approvalPolicy`:
|
||||
|
||||
- pending mode: queue suggestions; apply only after explicit approval
|
||||
- auto mode: apply safe workspace-skill updates when clearly reusable
|
||||
|
||||
## Costs and Runtime Behavior
|
||||
|
||||
Heuristic capture does not call a model.
|
||||
|
||||
LLM review uses an embedded run on the active/default agent model. It is
|
||||
threshold-based so it does not run on every turn by default.
|
||||
|
||||
The reviewer:
|
||||
|
||||
- uses the same configured provider/model context when available
|
||||
- falls back to runtime agent defaults
|
||||
- has `reviewTimeoutMs`
|
||||
- uses lightweight bootstrap context
|
||||
- has no tools
|
||||
- writes nothing directly
|
||||
- can only emit a proposal that goes through the normal scanner and
|
||||
approval/quarantine path
|
||||
|
||||
If the reviewer fails, times out, or returns invalid JSON, the plugin logs a
|
||||
warning/debug message and skips that review pass.
|
||||
|
||||
## Operating Patterns
|
||||
|
||||
Use Skill Workshop when the user says:
|
||||
|
||||
- “next time, do X”
|
||||
- “from now on, prefer Y”
|
||||
- “make sure to verify Z”
|
||||
- “save this as a workflow”
|
||||
- “this took a while; remember the process”
|
||||
- “update the local skill for this”
|
||||
|
||||
Good skill text:
|
||||
|
||||
```markdown
|
||||
## Workflow
|
||||
|
||||
- Verify the GIF URL resolves to `image/gif`.
|
||||
- Confirm the file has multiple frames.
|
||||
- Record source URL, license, and attribution.
|
||||
- Store a local copy when the asset will ship with the product.
|
||||
- Verify the local asset renders in the target UI before final reply.
|
||||
```
|
||||
|
||||
Poor skill text:
|
||||
|
||||
```markdown
|
||||
The user asked about a GIF and I searched two websites. Then one was blocked by
|
||||
Cloudflare. The final answer said to check attribution.
|
||||
```
|
||||
|
||||
Reasons the poor version should not be saved:
|
||||
|
||||
- transcript-shaped
|
||||
- not imperative
|
||||
- includes noisy one-off details
|
||||
- does not tell the next agent what to do
|
||||
|
||||
## Debugging
|
||||
|
||||
Check whether the plugin is loaded:
|
||||
|
||||
```bash
|
||||
openclaw plugins list --enabled
|
||||
```
|
||||
|
||||
Check proposal counts from an agent/tool context:
|
||||
|
||||
```json
|
||||
{ "action": "status" }
|
||||
```
|
||||
|
||||
Inspect pending proposals:
|
||||
|
||||
```json
|
||||
{ "action": "list_pending" }
|
||||
```
|
||||
|
||||
Inspect quarantined proposals:
|
||||
|
||||
```json
|
||||
{ "action": "list_quarantine" }
|
||||
```
|
||||
|
||||
Common symptoms:
|
||||
|
||||
| Symptom | Likely cause | Check |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| Tool is unavailable | Plugin entry is not enabled | `plugins.entries.skill-workshop.enabled` and `openclaw plugins list` |
|
||||
| No automatic proposal appears | `autoCapture: false`, `reviewMode: "off"`, or thresholds not met | Config, proposal status, Gateway logs |
|
||||
| Heuristic did not capture | User wording did not match correction patterns | Use explicit `skill_workshop.suggest` or enable LLM reviewer |
|
||||
| Reviewer did not create a proposal | Reviewer returned `none`, invalid JSON, or timed out | Gateway logs, `reviewTimeoutMs`, thresholds |
|
||||
| Proposal is not applied | `approvalPolicy: "pending"` | `list_pending`, then `apply` |
|
||||
| Proposal disappeared from pending | Duplicate proposal reused, max pending pruning, or was applied/rejected/quarantined | `status`, `list_pending` with status filters, `list_quarantine` |
|
||||
| Skill file exists but model misses it | Skill snapshot not refreshed or skill gating excludes it | `openclaw skills` status and workspace skill eligibility |
|
||||
|
||||
Relevant logs:
|
||||
|
||||
- `skill-workshop: queued <skill>`
|
||||
- `skill-workshop: applied <skill>`
|
||||
- `skill-workshop: quarantined <skill>`
|
||||
- `skill-workshop: heuristic capture skipped: ...`
|
||||
- `skill-workshop: reviewer skipped: ...`
|
||||
- `skill-workshop: reviewer found no update`
|
||||
|
||||
## QA Scenarios
|
||||
|
||||
Repo-backed QA scenarios:
|
||||
|
||||
- `qa/scenarios/plugins/skill-workshop-animated-gif-autocreate.md`
|
||||
- `qa/scenarios/plugins/skill-workshop-pending-approval.md`
|
||||
- `qa/scenarios/plugins/skill-workshop-reviewer-autonomous.md`
|
||||
|
||||
Run the deterministic coverage:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite \
|
||||
--scenario skill-workshop-animated-gif-autocreate \
|
||||
--scenario skill-workshop-pending-approval \
|
||||
--concurrency 1
|
||||
```
|
||||
|
||||
Run reviewer coverage:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite \
|
||||
--scenario skill-workshop-reviewer-autonomous \
|
||||
--concurrency 1
|
||||
```
|
||||
|
||||
The reviewer scenario is intentionally separate because it enables
|
||||
`reviewMode: "llm"` and exercises the embedded reviewer pass.
|
||||
|
||||
## When Not To Enable Auto Apply
|
||||
|
||||
Avoid `approvalPolicy: "auto"` when:
|
||||
|
||||
- the workspace contains sensitive procedures
|
||||
- the agent is working on untrusted input
|
||||
- skills are shared across a broad team
|
||||
- you are still tuning prompts or scanner rules
|
||||
- the model frequently handles hostile web/email content
|
||||
|
||||
Use pending mode first. Switch to auto mode only after reviewing the kind of
|
||||
skills the agent proposes in that workspace.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [Skills](/tools/skills)
|
||||
- [Plugins](/tools/plugin)
|
||||
- [Testing](/reference/test)
|
||||
@ -90,6 +90,24 @@ You can gate them via `metadata.openclaw.requires.config` on the plugin’s conf
|
||||
entry. See [Plugins](/tools/plugin) for discovery/config and [Tools](/tools) for the
|
||||
tool surface those skills teach.
|
||||
|
||||
## Skill Workshop
|
||||
|
||||
The optional, experimental Skill Workshop plugin can create or update workspace
|
||||
skills from reusable procedures observed during agent work. It is disabled by
|
||||
default and must be explicitly enabled through
|
||||
`plugins.entries.skill-workshop`.
|
||||
|
||||
Skill Workshop writes only to `<workspace>/skills`, scans generated content,
|
||||
supports pending approval or automatic safe writes, quarantines unsafe
|
||||
proposals, and refreshes the skill snapshot after successful writes so new
|
||||
skills can become available without a Gateway restart.
|
||||
|
||||
Use it when you want corrections such as “next time, verify GIF attribution” or
|
||||
hard-won workflows such as media QA checklists to become durable procedural
|
||||
instructions. Start with pending approval; use automatic writes only in trusted
|
||||
workspaces after reviewing its proposals. Full guide:
|
||||
[Skill Workshop Plugin](/plugins/skill-workshop).
|
||||
|
||||
## ClawHub (install + sync)
|
||||
|
||||
ClawHub is the public skills registry for OpenClaw. Browse at
|
||||
|
||||
Loading…
Reference in New Issue
Block a user