-
Notifications
You must be signed in to change notification settings - Fork 0
Add cortex insights tool #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| { | ||
| "id": "cortex", | ||
| "name": "Memory (Cortex)", | ||
| "description": "Cortex memory engine — retrieval, storage, and lifecycle management for evaOS", | ||
| "description": "Cortex memory engine \u2014 retrieval, storage, and lifecycle management for evaOS", | ||
| "version": "1.0.0", | ||
| "kind": "memory", | ||
| "main": "dist/index.js", | ||
|
|
@@ -15,6 +15,7 @@ | |
| "cortex_add_commitment", | ||
| "cortex_update_commitment", | ||
| "cortex_list_commitments", | ||
| "cortex_insights", | ||
| "cortex_add_open_loop", | ||
| "cortex_resolve_open_loop", | ||
| "cortex_list_open_loops" | ||
|
|
@@ -45,7 +46,7 @@ | |
| }, | ||
| "shadowMode": { | ||
| "type": "boolean", | ||
| "description": "Shadow mode — capture runs extraction but skips storage (dry-run, default: false)" | ||
| "description": "Shadow mode \u2014 capture runs extraction but skips storage (dry-run, default: false)" | ||
| }, | ||
| "retrievalBudget": { | ||
| "type": "number", | ||
|
|
@@ -57,10 +58,38 @@ | |
| }, | ||
| "retrievalMode": { | ||
| "type": "string", | ||
| "enum": ["auto", "fast", "thorough"], | ||
| "enum": [ | ||
| "auto", | ||
| "fast", | ||
| "thorough" | ||
| ], | ||
| "description": "Retrieval mode for memory search (default: fast)" | ||
| }, | ||
| "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" | ||
| } | ||
|
Comment on lines
+67
to
91
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| }, | ||
| "required": [] | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -358,6 +358,14 @@ class CortexClient { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.get<{ commitments?: Record<string, unknown>[]; total?: number }>(`/api/v1/commitments?${params}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --- Insights --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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))}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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))}`, | |
| 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
AI
Apr 14, 2026
There was a problem hiding this comment.
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.
| 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))}`, |
There was a problem hiding this comment.
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.
Was this helpful? React with 👍 or 👎 to provide feedback.
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
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.
| 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
AI
Apr 14, 2026
There was a problem hiding this comment.
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.
| const conf = typeof insight.confidence === "number" ? ` (${Math.round(insight.confidence * 100)}%)` : ""; | |
| const conf = Number.isFinite(insight.confidence) ? ` (${Math.round(insight.confidence * 100)}%)` : ""; |
Copilot
AI
Apr 14, 2026
There was a problem hiding this comment.
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.
| 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}`; |
There was a problem hiding this comment.
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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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_insightstool 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.