Skip to content
Open
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
55 changes: 55 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Pytest import helpers for the standalone plugin checkout."""

from __future__ import annotations

import importlib
import importlib.util
import os
import pathlib
import sys
import types


_PROJECT_ROOT = pathlib.Path(__file__).resolve().parent
_HERMES_PLUGIN_ROOT = _PROJECT_ROOT / "hermes-plugin"
if str(_HERMES_PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(_HERMES_PLUGIN_ROOT))


def _add_real_hermes_agent_root() -> None:
candidates = []
env_root = os.environ.get("HERMES_AGENT_ROOT")
if env_root:
candidates.append(pathlib.Path(env_root))
candidates.append(_PROJECT_ROOT.parent / "hermes-agent")

for candidate in candidates:
if (candidate / "agent").is_dir() and str(candidate) not in sys.path:
sys.path.insert(0, str(candidate))


def _real_memory_provider_available() -> bool:
if "agent.memory_provider" in sys.modules:
return True
_add_real_hermes_agent_root()
try:
spec = importlib.util.find_spec("agent.memory_provider")
except ModuleNotFoundError:
return False
if spec is None:
return False
importlib.import_module("agent.memory_provider")
return True


if not _real_memory_provider_available():
agent_module = types.ModuleType("agent")
memory_provider_module = types.ModuleType("agent.memory_provider")

class MemoryProvider:
pass

memory_provider_module.MemoryProvider = MemoryProvider
agent_module.memory_provider = memory_provider_module
sys.modules.setdefault("agent", agent_module)
sys.modules["agent.memory_provider"] = memory_provider_module
13 changes: 12 additions & 1 deletion hermes-plugin/memory/memory_tencentdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,17 @@ def _coerce_limit(
return value


def _recall_context_from_response(result: Dict[str, Any]) -> str:
"""Prefer split Gateway recall fields, falling back to legacy context."""
if "appendSystemContext" in result or "prependContext" in result:
parts = [
result.get("appendSystemContext") or "",
result.get("prependContext") or "",
]
return "\n\n".join(part for part in parts if part)
return result.get("context", "") or ""


# ---------------------------------------------------------------------------
# Tool schemas
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -804,7 +815,7 @@ def prefetch(self, query: str, *, session_id: str = "") -> str:
session_key=effective_session,
user_id=self._user_id,
)
context = result.get("context", "")
context = _recall_context_from_response(result)
self._record_success()
if context:
return f"## memory-tencentdb Memory\n{context}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Tests for Gateway /recall context field compatibility."""

from __future__ import annotations

from unittest.mock import MagicMock


from memory.memory_tencentdb import MemoryTencentdbProvider


def _ready_provider() -> MemoryTencentdbProvider:
provider = MemoryTencentdbProvider()
provider._client = MagicMock()
provider._gateway_available = True
provider._session_id = "test-session"
provider._user_id = "test-user"
provider._ensure_alive_for_request = MagicMock(return_value=True)
return provider


def test_prefetch_prefers_split_context_fields_over_legacy_context():
provider = _ready_provider()
provider._client.recall.return_value = {
"appendSystemContext": "stable system context",
"prependContext": "dynamic L1 context",
"context": "stale legacy context",
}

assert provider.prefetch("hello") == (
"## memory-tencentdb Memory\n"
"stable system context\n\n"
"dynamic L1 context"
)


def test_prefetch_reads_legacy_context_when_split_fields_absent():
provider = _ready_provider()
provider._client.recall.return_value = {"context": "legacy context"}

assert provider.prefetch("hello") == (
"## memory-tencentdb Memory\n"
"legacy context"
)
42 changes: 42 additions & 0 deletions src/gateway/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import type { RecallResult } from "../core/types.js";
import { buildRecallResponse } from "./server.js";

describe("buildRecallResponse", () => {
it("keeps prepend-only recall context in the legacy context field", () => {
const result: RecallResult = {
prependContext: "L1 dynamic memory",
recalledL1Memories: [
{ content: "L1 dynamic memory", score: 0.91, type: "episodic" },
],
recallStrategy: "hybrid",
};

expect(buildRecallResponse(result)).toEqual({
context: "L1 dynamic memory",
prependContext: "L1 dynamic memory",
strategy: "hybrid",
memory_count: 1,
});
});

it("returns split recall fields and joins legacy context as append then prepend", () => {
const result: RecallResult = {
appendSystemContext: "stable system context",
prependContext: "dynamic L1 context",
recalledL1Memories: [
{ content: "first", score: 0.87, type: "episodic" },
{ content: "second", score: 0.74, type: "instruction" },
],
recallStrategy: "keyword",
};

expect(buildRecallResponse(result)).toEqual({
context: "stable system context\n\ndynamic L1 context",
appendSystemContext: "stable system context",
prependContext: "dynamic L1 context",
strategy: "keyword",
memory_count: 2,
});
});
});
33 changes: 25 additions & 8 deletions src/gateway/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,31 @@ import type {
SeedResponse,
GatewayErrorResponse,
} from "./types.js";
import type { Logger } from "../core/types.js";
import type { Logger, RecallResult } from "../core/types.js";
import { validateAndNormalizeRaw, fillTimestamps, SeedValidationError } from "../core/seed/input.js";
import { executeSeed } from "../core/seed/seed-runtime.js";
import type { SeedProgress } from "../core/seed/types.js";

const TAG = "[tdai-gateway]";
const VERSION = "0.1.0";

function joinRecallContext(appendSystemContext?: string, prependContext?: string): string {
return [appendSystemContext, prependContext]
.filter((part): part is string => Boolean(part))
.join("\n\n");
}

export function buildRecallResponse(result: RecallResult): RecallResponse {
const { appendSystemContext, prependContext } = result;
return {
context: joinRecallContext(appendSystemContext, prependContext),
...(prependContext !== undefined ? { prependContext } : {}),
...(appendSystemContext !== undefined ? { appendSystemContext } : {}),
strategy: result.recallStrategy,
memory_count: result.recalledL1Memories?.length ?? 0,
};
}

// ============================
// Console logger (for standalone gateway — no OpenClaw logger available)
// ============================
Expand Down Expand Up @@ -238,14 +255,14 @@ export class TdaiGateway {
const startMs = Date.now();
const result = await this.core.handleBeforeRecall(body.query, body.session_key);
const elapsed = Date.now() - startMs;
const response = buildRecallResponse(result);

this.logger.info(`Recall completed in ${elapsed}ms: context=${(result.appendSystemContext?.length ?? 0)} chars`);

const response: RecallResponse = {
context: result.appendSystemContext ?? "",
strategy: result.recallStrategy,
memory_count: result.recalledL1Memories?.length ?? 0,
};
this.logger.info(
`Recall completed in ${elapsed}ms: ` +
`appendSystemContext=${response.appendSystemContext?.length ?? 0} chars, ` +
`prependContext=${response.prependContext?.length ?? 0} chars, ` +
`context=${response.context.length} chars`,
);
sendJson(res, 200, response);
}

Expand Down
2 changes: 2 additions & 0 deletions src/gateway/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface RecallRequest {

export interface RecallResponse {
context: string;
prependContext?: string;
appendSystemContext?: string;
strategy?: string;
memory_count?: number;
}
Expand Down