Skip to content

Add cortex insights tool#12

Merged
100yenadmin merged 1 commit into
mainfrom
feat/insights-api-tool
Apr 14, 2026
Merged

Add cortex insights tool#12
100yenadmin merged 1 commit into
mainfrom
feat/insights-api-tool

Conversation

@100yenadmin
Copy link
Copy Markdown
Member

@100yenadmin 100yenadmin commented Apr 14, 2026

Summary

  • add CortexClient.listInsights for the new insights API
  • register cortex_insights tool and expose it in plugin manifest
  • rebuild dist artifacts

Testing

  • npm run build

Open with Devin

Summary by CodeRabbit

  • New Features
    • Added cortex_insights tool to retrieve and list insights with customizable status filtering and result limits.
    • Extended configuration options including injection format selection, conflict and relation visibility toggles, and deduplication control.

Copilot AI review requested due to automatic review settings April 14, 2026 22:15
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a new cortex_insights tool to the plugin that fetches insights from an API endpoint. Includes HTTP client method implementation with configurable status and limit parameters, error handling, and response formatting. Updates plugin configuration schema with new optional fields.

Changes

Cohort / File(s) Summary
Plugin Manifest
openclaw.plugin.json
Added cortex_insights tool entry; extended configSchema with injectionFormat, showConflicts, showRelations, and dedup fields; standardized em dash Unicode escaping in descriptions.
Client Implementation
src/index.ts
Added listInsights() HTTP client method with status/limit parameters; registered cortex_insights tool with execution logic including response formatting and error handling.

Sequence Diagram

sequenceDiagram
    participant Tool as Tool Executor
    participant Client as HTTP Client
    participant API as Insights API
    participant Formatter as Response Formatter

    Tool->>Tool: Extract params (status, limit)
    Tool->>Client: listInsights(status, limit)
    Client->>API: GET /api/v1/insights?owner_id=...&status=...&limit=...
    API-->>Client: { insights?: [...], count?: ... }
    
    alt insights found and valid
        Client-->>Formatter: insights array
        Formatter->>Formatter: Format with tags
        Formatter-->>Tool: formatted string list
    else empty result
        Formatter-->>Tool: "No insights found."
    else null/undefined result
        Formatter-->>Tool: "Failed to fetch insights."
    else error thrown
        Tool-->>Tool: catch error
        Tool-->>Tool: "List insights failed: ..."
    end
    
    Tool-->>Tool: Return result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change—adding a new cortex insights tool to the plugin.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/insights-api-tool

Comment @coderabbitai help to get the list of available commands and usage tips.

@100yenadmin 100yenadmin merged commit 5094552 into main Apr 14, 2026
4 of 6 checks passed
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread src/index.ts
Comment on lines +363 to +367
async listInsights(status = "pending", limit = 5) {
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: listInsights uses manual URL encoding instead of URLSearchParams

The new listInsights method at src/index.ts:363-367 manually constructs the query string using encodeURIComponent, whereas all other list methods (listCommitments at src/index.ts:355-358, listOpenLoops at src/index.ts:385-388, listContradictions at src/index.ts:325-329) use URLSearchParams. Both approaches produce correct output, but the inconsistency makes the codebase slightly harder to maintain. Not a bug, but a style divergence worth noting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/index.ts
Comment on lines +1710 to +1745
api.registerTool(
{
name: "cortex_insights",
label: "Cortex Insights",
description: "List cross-system insights from behavioral + memory analysis. Shows patterns, correlations, and recommendations discovered by the dreaming engine.",
parameters: Type.Object({
status: Type.Optional(Type.String({ description: "Filter by status: pending, accepted, or all" })),
limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5)" })),
}),
async execute(_toolCallId: string, params: unknown): Promise<any> {
const { status, limit } = params as { status?: string; limit?: number };
try {
const result = await client.listInsights(status ?? "pending", limit ?? 5);
if (!result) {
return { content: [{ type: "text" as const, text: "Failed to fetch insights." }] };
}
const items = (result as any)?.insights ?? [];
if (!items.length) {
return { content: [{ type: "text" as const, text: "No insights found." }] };
}
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const type = insight.insight_type ? ` [${insight.insight_type}]` : "";
const statusTag = insight.status ? ` [${insight.status}]` : "";
return `${i + 1}. ${insight.insight ?? insight.content ?? JSON.stringify(insight)}${conf}${type}${statusTag}`;
})
.join("\n");
return { content: [{ type: "text" as const, text: `Found ${items.length} insight(s):\n\n${text}` }] };
} catch (err) {
return { content: [{ type: "text" as const, text: `List insights failed: ${String(err)}` }] };
}
},
},
{ name: "cortex_insights" },
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: cortex_insights tool does not expose owner_id parameter unlike peer tools

Most similar tools (e.g., cortex_list_commitments, cortex_list_open_loops, cortex_list_contradictions) expose an optional owner_id parameter allowing callers to override the default owner namespace. The cortex_insights tool and its underlying listInsights method always use this.ownerId with no override. This may be intentional (insights are always scoped to the configured owner) but it's a deviation from the pattern of the sibling tools. If multi-owner insight queries are ever needed, this would require a change.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread openclaw.plugin.json
Comment on lines +67 to 91
},
"injectionFormat": {
"type": "string",
"enum": [
"v1",
"v2"
],
"default": "v1",
"description": "Memory injection format version"
},
"showConflicts": {
"type": "boolean",
"default": true,
"description": "Show conflict markers in v2 format"
},
"showRelations": {
"type": "boolean",
"default": true,
"description": "Show relation hints in v2 format"
},
"dedup": {
"type": "boolean",
"default": true,
"description": "Deduplicate similar memories in v2 format"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Config schema in openclaw.plugin.json catches up to already-shipped code

The new config properties (injectionFormat, showConflicts, showRelations, dedup) added to openclaw.plugin.json are already present in the EvaMemoryConfig interface and parseConfig at src/index.ts:55-58 and src/index.ts:122-125, shipped in the prior PR #10 (feat: add v2 memory injection formatting). This PR is simply aligning the JSON schema declaration with the already-functional code, which is a good catch-up.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Cortex “insights” capability to the OpenClaw Cortex plugin by extending the HTTP client, registering a new tool, and updating published artifacts/manifest to expose it.

Changes:

  • Add CortexClient.listInsights() for the /api/v1/insights endpoint.
  • Register a new cortex_insights tool and include it in openclaw.plugin.json.
  • Rebuild dist/ outputs.

Reviewed changes

Copilot reviewed 2 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/index.ts Adds the listInsights client method and registers the cortex_insights tool.
openclaw.plugin.json Exposes cortex_insights in the manifest and updates config schema formatting/fields.
dist/index.js Compiled JS including the new insights client + tool registration.
dist/index.js.map Updated sourcemap for rebuilt JS bundle.
dist/index.d.ts.map Updated declaration map for rebuilt typings output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread openclaw.plugin.json
Comment on lines +68 to +90
"injectionFormat": {
"type": "string",
"enum": [
"v1",
"v2"
],
"default": "v1",
"description": "Memory injection format version"
},
"showConflicts": {
"type": "boolean",
"default": true,
"description": "Show conflict markers in v2 format"
},
"showRelations": {
"type": "boolean",
"default": true,
"description": "Show relation hints in v2 format"
},
"dedup": {
"type": "boolean",
"default": true,
"description": "Deduplicate similar memories in v2 format"
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions exposing the new cortex_insights tool in the manifest, but this diff also adds new config schema properties (injectionFormat, showConflicts, showRelations, dedup). If those additions are intentional, please update the PR description to reflect the broader manifest/config change so reviewers/users know about the new config options.

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
Comment on lines +364 to +365
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listInsights builds the query string manually, unlike the other list* methods that use URLSearchParams. To keep encoding consistent and avoid subtle query-string bugs as parameters evolve, build the URL with new URLSearchParams({ owner_id: ..., status: ..., limit: ... }) and interpolate ?${params} (similar to listCommitments / listOpenLoops).

Suggested change
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
const params = new URLSearchParams({
owner_id: this.ownerId,
status,
limit: String(limit),
});
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?${params}`,

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
Comment on lines +363 to +365
async listInsights(status = "pending", limit = 5) {
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listInsights is the only list-style CortexClient method here that does not accept an optional ownerId parameter (the other list methods do). If insights are namespaced like the other resources, consider adding ownerId?: string and defaulting to this.ownerId for API consistency and future flexibility.

Suggested change
async listInsights(status = "pending", limit = 5) {
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,
async listInsights(ownerId?: string, status = "pending", limit = 5) {
return this.get<{ insights?: Record<string, unknown>[]; count?: number }>(
`/api/v1/insights?owner_id=${encodeURIComponent(ownerId ?? this.ownerId)}&status=${encodeURIComponent(status)}&limit=${encodeURIComponent(String(limit))}`,

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
Comment on lines +1716 to +1722
status: Type.Optional(Type.String({ description: "Filter by status: pending, accepted, or all" })),
limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5)" })),
}),
async execute(_toolCallId: string, params: unknown): Promise<any> {
const { status, limit } = params as { status?: string; limit?: number };
try {
const result = await client.listInsights(status ?? "pending", limit ?? 5);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tool schema allows any string status and any numeric limit (including negative / non-integer / very large values). Since these directly control the API query and output size, constrain them in the TypeBox schema (e.g., literals/union for status, and an integer with min/max for limit) and/or clamp/validate before calling client.listInsights.

Suggested change
status: Type.Optional(Type.String({ description: "Filter by status: pending, accepted, or all" })),
limit: Type.Optional(Type.Number({ description: "Max results to return (default: 5)" })),
}),
async execute(_toolCallId: string, params: unknown): Promise<any> {
const { status, limit } = params as { status?: string; limit?: number };
try {
const result = await client.listInsights(status ?? "pending", limit ?? 5);
status: Type.Optional(
Type.Union(
[Type.Literal("pending"), Type.Literal("accepted"), Type.Literal("all")],
{ description: "Filter by status: pending, accepted, or all" },
),
),
limit: Type.Optional(
Type.Integer({ minimum: 1, maximum: 50, description: "Max results to return (default: 5, max: 50)" }),
),
}),
async execute(_toolCallId: string, params: unknown): Promise<any> {
const { status, limit } = params as {
status?: "pending" | "accepted" | "all";
limit?: number;
};
try {
const safeStatus = status === "accepted" || status === "all" || status === "pending" ? status : "pending";
const safeLimit = Number.isInteger(limit) ? Math.min(50, Math.max(1, limit)) : 5;
const result = await client.listInsights(safeStatus, safeLimit);

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
}
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typeof insight.confidence === "number" will also be true for NaN/Infinity, which can produce user-visible NaN%/Infinity% in output. Use a finite-number check (e.g., Number.isFinite) before formatting the percentage.

Suggested change
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const conf = Number.isFinite(insight.confidence) ? ` (${Math.round(insight.confidence * 100)}%)` : "";

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
Comment on lines +1730 to +1735
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const type = insight.insight_type ? ` [${insight.insight_type}]` : "";
const statusTag = insight.status ? ` [${insight.status}]` : "";
return `${i + 1}. ${insight.insight ?? insight.content ?? JSON.stringify(insight)}${conf}${type}${statusTag}`;
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Falling back to JSON.stringify(insight) can emit very large blobs and may unintentionally surface fields that shouldn't be shown to the user. Prefer selecting a small, known-safe subset of fields (and truncating long text) for the fallback case to keep responses bounded and predictable.

Suggested change
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const type = insight.insight_type ? ` [${insight.insight_type}]` : "";
const statusTag = insight.status ? ` [${insight.status}]` : "";
return `${i + 1}. ${insight.insight ?? insight.content ?? JSON.stringify(insight)}${conf}${type}${statusTag}`;
const truncateText = (value: unknown, max = 200): string | undefined => {
if (typeof value !== "string") return undefined;
const normalized = value.trim();
if (!normalized) return undefined;
return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
};
const formatInsightFallback = (insight: any): string => {
const parts = [
truncateText(insight?.title, 80),
truncateText(insight?.summary, 160),
typeof insight?.id === "string" && insight.id.trim() ? `id=${insight.id.trim()}` : undefined,
typeof insight?.insight_type === "string" && insight.insight_type.trim()
? `type=${insight.insight_type.trim()}`
: undefined,
typeof insight?.status === "string" && insight.status.trim() ? `status=${insight.status.trim()}` : undefined,
typeof insight?.confidence === "number" ? `confidence=${Math.round(insight.confidence * 100)}%` : undefined,
].filter((part): part is string => Boolean(part));
return parts.length ? parts.join(" | ") : "Insight details unavailable";
};
const text = items
.map((insight: any, i: number) => {
const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : "";
const type = insight.insight_type ? ` [${insight.insight_type}]` : "";
const statusTag = insight.status ? ` [${insight.status}]` : "";
const displayText =
truncateText(insight.insight, 280) ??
truncateText(insight.content, 280) ??
formatInsightFallback(insight);
return `${i + 1}. ${displayText}${conf}${type}${statusTag}`;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants