Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions examples/openclaw-contextengine-plugin/INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Install OpenViking Context Engine for OpenClaw

This plugin is an example context-engine extension located in `examples/openclaw-contextengine-plugin`.

## 1) Copy plugin files to OpenClaw extension directory

```bash
mkdir -p ~/.openclaw/extensions/contextengine-openviking
cp examples/openclaw-contextengine-plugin/{index.ts,types.ts,config.ts,client.ts,retrieval.ts,injection.ts,ingestion.ts,tools.ts,skill-tool-memory.ts,fallback.ts,telemetry.ts,openclaw.plugin.json,package.json,tsconfig.json} \
~/.openclaw/extensions/contextengine-openviking/
```

## 2) Install dependencies in plugin directory

```bash
cd ~/.openclaw/extensions/contextengine-openviking
npm install
```

## 3) Enable plugin and assign context-engine slot

```bash
openclaw config set plugins.enabled true
openclaw config set plugins.slots.contextEngine contextengine-openviking
openclaw config set plugins.entries.contextengine-openviking.config.mode "local"
openclaw config set plugins.entries.contextengine-openviking.config.retrieval.enabled true --json
openclaw config set plugins.entries.contextengine-openviking.config.retrieval.injectMode "simulated_tool_result"
openclaw config set plugins.entries.contextengine-openviking.config.retrieval.scoreThreshold 0.15
openclaw config set plugins.entries.contextengine-openviking.config.ingestion.writeMode "compact_batch"
openclaw config set plugins.entries.contextengine-openviking.config.ingestion.maxBatchMessages 200
```

## 4) Start OpenClaw

```bash
openclaw gateway
```

## 5) Verify

- Confirm plugin slot is configured:

```bash
openclaw config get plugins.slots.contextEngine
```

- Run local tests in the plugin source folder:

```bash
cd /path/to/OpenViking/examples/openclaw-contextengine-plugin
pnpm exec vitest
```

## Notes

- This example uses OpenViking HTTP endpoint `http://127.0.0.1:1933` by default.
- Retrieval failures are handled gracefully and do not block assembly.
43 changes: 43 additions & 0 deletions examples/openclaw-contextengine-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# OpenClaw OpenViking Context Engine Plugin

Use OpenViking retrieval during context assembly for OpenClaw sessions. This plugin adds retrieval-aware prompt augmentation plus memory commit/search tools.

## What it provides

- Context engine id: `contextengine-openviking`
- Retrieval pipeline:
- Build query from recent user turns
- Filter/rank by score threshold and top-k
- Inject as text or simulated tool-result block
- Graceful fallback behavior when retrieval fails (timeouts/errors)
- Tools:
- `commit_memory`
- `search_memories`

## Files

- Plugin manifest: `openclaw.plugin.json`
- Entry point: `index.ts`
- Engine lifecycle: `context-engine.ts`
- OpenViking client: `client.ts`
- Retrieval/injection/ingestion helpers: `retrieval.ts`, `injection.ts`, `ingestion.ts`
- Fallback + telemetry helpers: `fallback.ts`, `telemetry.ts`

## Test status

Run from this folder:

```bash
pnpm exec vitest
```

Current coverage in this example includes:

- config parsing and bounds
- HTTP client behavior (headers, timeouts, error paths)
- retrieval ranking and dedupe
- injection formatting and truncation boundaries
- ingestion batching and commit flow
- context-engine lifecycle behavior
- plugin registration and tool exposure
- fallback classification + graceful degradation integration
146 changes: 146 additions & 0 deletions examples/openclaw-contextengine-plugin/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import { createOpenVikingClient } from "./client.js";

describe("OpenViking client", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("normalizes base URL without trailing slash", () => {
const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933/",
timeoutMs: 15000,
apiKey: "",
});

expect(c.baseUrl).toBe("http://127.0.0.1:1933");
});

it("calls health endpoint with auth and agent headers", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ status: "ok" }),
} as Response);

const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933/",
timeoutMs: 15000,
apiKey: "secret",
agentId: "agent-1",
});

await expect(c.health()).resolves.toBe(true);

const call = fetchMock.mock.calls[0];
expect(call?.[0]).toBe("http://127.0.0.1:1933/health");
const headers = new Headers((call?.[1] as RequestInit | undefined)?.headers);
expect(headers.get("Content-Type")).toBe("application/json");
expect(headers.get("Authorization")).toBe("Bearer secret");
expect(headers.get("X-OpenViking-Agent")).toBe("agent-1");
});

it("throws on non-ok responses", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: false,
status: 500,
json: async () => ({ message: "boom" }),
} as Response);

const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933",
timeoutMs: 15000,
});

await expect(c.find("hello")).rejects.toThrow(/OpenViking request failed \(500\) on \/api\/v1\/search\/find/);
});

it("aborts request when timeout is exceeded", async () => {
let aborted = false;
vi.spyOn(globalThis, "fetch").mockImplementation((_, init) => {
return new Promise((_, reject) => {
const signal = (init as RequestInit | undefined)?.signal;
signal?.addEventListener("abort", () => {
aborted = true;
reject(new Error("aborted"));
});
});
});

const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933",
timeoutMs: 1,
});

await expect(c.find("slow")).rejects.toThrow(/request timeout after 1ms/);
expect(aborted).toBe(true);
});

it("unwraps OpenViking envelope result for session creation", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
status: "ok",
result: { session_id: "s-envelope" },
}),
} as Response);

const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933",
timeoutMs: 15000,
});

await expect(c.createSession()).resolves.toBe("s-envelope");
});

it("throws when OpenViking envelope returns non-ok status", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
status: "error",
error: "upstream unavailable",
}),
} as Response);

const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933",
timeoutMs: 15000,
});

await expect(c.find("hello")).rejects.toThrow(/upstream unavailable/);
});
it("maps commit extracted_count to extractedCount", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ extracted_count: 3 }),
} as Response);

const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933",
timeoutMs: 15000,
});

await expect(c.commitSession("s1")).resolves.toEqual({ extractedCount: 3 });
});

it("handles 204 responses without parsing json", async () => {
const json = vi.fn(async () => ({}));
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
status: 204,
json,
} as unknown as Response);

const c = createOpenVikingClient({
baseUrl: "http://127.0.0.1:1933",
timeoutMs: 15000,
});

await expect(c.deleteSession("s1")).resolves.toBeUndefined();
expect(json).not.toHaveBeenCalled();
});
});
Loading
Loading