Voice
Social Learning
OpenClaw plugin that makes an agent fit a group's conversational voice via the Humalike Social Learning API. OpenClaw counterpart of the Hermes plugin.
Install
npm install &&
Configuration Example
{
"plugins": {
"entries": {
"social-learning": {
"config": {
"apiKey": "ak_live_...", // or set HUMALIKE_API_KEY instead
"serviceUrl": "https://api.humalike.com",
"refreshEveryTurns": 5,
"maxMessages": 100,
"minMessages": 4,
"injectInto": "user", // "user" (preserves prompt caching) | "system"
"agentSpeakerName": "assistant",
"requestTimeoutMs": 8000,
"logRequests": false
}
}
}
}
}
README
# Humalike Social Learning β OpenClaw plugin
Make an OpenClaw agent **fit the room**. This plugin sends the recent
conversation to the [Humalike Social Learning API](https://api.humalike.com),
which returns a *voice card* β a tight set of norms, tone, formatting, and slang
rules recovered from how the group actually talks β and injects that card into
the agent's turn so its replies match the community instead of sounding like a
generic assistant.
It is the OpenClaw counterpart of the
[Hermes plugin](https://github.com/maksbilski/hermes-social-learning): same
**observe β extract β cache β inject** loop, rebuilt on OpenClaw's plugin SDK.
## How it works
```
every turn (before_prompt_build)
session history βββββββββββββΊ map to transcript βββΊ inject cached voice card
β β²
on a cadenceβ (background, non-blocking)
βΌ β
POST /v1/social-learning/actions/extract ββββββ
β { profile, prompt_block }
```
1. On every turn, OpenClaw's `before_prompt_build` hook hands the plugin the
loaded session history. The plugin maps it to a Humalike transcript
(`{ id, speaker, text }`).
2. Every `refreshEveryTurns` turns it calls the Social Learning API **in the
background** and caches the returned `prompt_block` per thread.
3. The cached card is injected into the agent's turn (`appendContext` by
default). The turn itself **never waits on the network**: until the first
extraction lands, the agent behaves exactly as it would without the plugin.
The API does all the social reasoning; the plugin only handles observation,
caching, refresh scheduling, and injection.
> **Scope: one capability, on purpose.** This plugin wires *only* the Social
> Learning API β the one Humalike capability whose contract is verified and that
> fits the "inject context before the turn" model (just like the Hermes plugin).
> The other Humalike APIs need a different integration shape (Theory of Mind
> refines a *draft* reply; Turn-taking is stateful over WebSocket; Persona is
> async; Social Observability is too slow for a live turn) and are intentionally
> out of scope here.
## Install
From source (works today β the host loads the built `dist/`):
```bash
git clone https://github.com/wsobinski/openclaw-social-learning.git
cd openclaw-social-learning
npm install && npm run build
openclaw plugins install .
openclaw plugins enable social-learning
openclaw gateway restart
openclaw plugins inspect social-learning --runtime --json # expect status: loaded, hookCount: 2
```
Once published to ClawHub:
```bash
openclaw plugins install clawhub:social-learning
openclaw plugins enable social-learning
openclaw gateway restart
```
## Configure
Set your Humalike API key (`ak_...`) either in plugin config or via the
`HUMALIKE_API_KEY` environment variable. Config lives under
`plugins.entries.social-learning.config`:
```jsonc
{
"plugins": {
"entries": {
"social-learning": {
"config": {
"apiKey": "ak_live_...", // or set HUMALIKE_API_KEY instead
"serviceUrl": "https://api.humalike.com",
"refreshEveryTurns": 5,
"maxMessages": 100,
"minMessages": 4,
"injectInto": "user", // "user" (preserves prompt caching) | "system"
"agentSpeakerName": "assistant",
"requestTimeoutMs": 8000,
"logRequests": false
}
}
}
}
}
```
| Option | Default | Meaning |
|---|---|---|
| `apiKey` | β | Humalike API key (`ak_...`). Falls back to `HUMALIKE_API_KEY`. |
| `serviceUrl` | `https://api.humalike.com` | Base URL of the Humalike API. |
| `refreshEveryTurns` | `5` | Re-extract the voice card after this many turns (Hermes' fixed cadence, made configurable). |
| `maxMessages` | `100` | Most-recent messages sent per extraction. |
| `minMessages` | `4` | Don't extract until the thread has at least this much signal. |
| `injectInto` | `user` | `user` β append to user-turn context (keeps the system prompt cache-stable). `system` β append to the system prompt. |
| `agentSpeakerName` | `assistant` | Label for the agent's own messages in the transcript. |
| `requestTimeoutMs` | `8000` | Per-request timeout for extraction. |
| `logRequests` | `false` | Debug-log each extraction. |
> **Why `injectInto: "user"` is the default.** The voice card changes every few
> turns. Appending it to a stable system prompt would invalidate the
> system-prompt cache on every refresh, so by default the card rides in the
> user-turn context instead β the same trade-off the Hermes plugin makes.
## Relationship to the Hermes plugin
This is a **faithful port** of the
[Hermes plugin](https://github.com/maksbilski/hermes-social-learning): the same
observe β extract β cache β inject loop, the same API contract, the same default
cadence (every 5 turns over the last 100 messages), the same append-to-user-turn
injection, and the same failure handling (a failed extraction is skipped and
retried on the next cycle β no backoff, no TTL). It targets the Social Learning
API exactly as the contract ships on the monorepo's `main` branch:
`POST /v1/social-learning/actions/extract`, `Authorization: Bearer ak_...`, body
`{ transcript: { messages: [{ id, speaker, text }] } }` (each message's `id` is
required), response `{ profile, prompt_block }`.
The only differences from Hermes are **forced by how you set things up in
OpenClaw vs. the Hermes Agent framework**:
- **TypeScript + a plugin manifest/config schema** instead of Python β OpenClaw's
plugin packaging.
- **Real session history from the `before_prompt_build` hook** instead of Hermes'
own message-buffering β OpenClaw hands the plugin the loaded history directly.
- **Per-thread caching** β one OpenClaw gateway serves many independent
channels/DMs at once, so each session keeps its own voice card (a single shared
card would bleed one room's voice into another).
- **Non-blocking background refresh + in-flight dedupe** β the hook runs on the
live turn path and OpenClaw can fire overlapping turns, so the refresh runs in
the background (the turn never stalls) and at most one runs per thread. The
observable behaviour is identical to Hermes.
## Develop
```bash
npm install
npm run typecheck # tsc --strict over the plugin source
npm test # node:test suite (transcript / client / config / refresh + real-entry integration)
npm run build # emit dist/
```
## Verified
This plugin has been exercised against the real OpenClaw runtime, not just unit tests:
- **Loads in the real host.** `openclaw plugins install` + `enable` (OpenClaw
2026.6.9) loads `dist/index.js`, runs `register(api)`, and
`openclaw plugins inspect social-learning --runtime` reports `status: "loaded"`,
`activated: true`, `shape: "hook-only"`, `hookCount: 2` (the
`before_prompt_build` + `gateway_stop` hooks), and the parsed `configSchema` +
UI hints. A missing key disables the plugin loudly
(`social-learning disabled: no Humalike API key β¦`) instead of crashing the gateway.
- **End-to-end inject path.** `tests/integration.test.ts` drives the actual
built entry through two turns with a stubbed `fetch`: it confirms both hooks
register, that turn 1 POSTs the mapped transcript to
`/v1/social-learning/actions/extract` with `Authorization: Bearer β¦`, and that
turn 2 returns the cached card as `appendContext`.
- **Live contract.** `POST https://api.humalike.com/v1/social-learning/actions/extract`
without a key returns `401` β confirming the route, method, and bearer-auth
contract this plugin targets.
> The OpenClaw host CLI requires Node β₯ 22.19; the plugin's own build/test
> toolchain runs on Node β₯ 22.
The entry is typed against the public SDK, per the official plugin docs:
`openclaw` is a dev dependency, and `src/index.ts` imports `definePluginEntry`
from `openclaw/plugin-sdk/plugin-entry` and registers the typed
`before_prompt_build` hook via `api.on(...)`. `npm run build` emits
`dist/index.js`, which `package.json` `openclaw.runtimeExtensions` points the
host at (so installs don't compile TypeScript at runtime), and the manifest's
`activation.onStartup: true` ensures the hook plugin loads at gateway startup.
Keep `openclaw.compat.pluginApi` in step with the `openclaw` version you build
against.
## Layout
```
openclaw.plugin.json native manifest (id, activation, configSchema, uiHints)
package.json "openclaw" block: source entry + runtimeExtensions + compat/build
src/
index.ts entry: definePluginEntry + api.on("before_prompt_build")
config.ts defaults, env merge, validation
client.ts Humalike Social Learning API client (injected fetch)
transcript.ts session history (unknown[]) β Humalike transcript (pure)
voice-card-store.ts per-thread cache + background refresh engine (pure)
tests/ node:test unit suites
```
## License
MIT β see [LICENSE](./LICENSE).
voice
Comments
Sign in to leave a comment