-
Notifications
You must be signed in to change notification settings - Fork 656
fix(pi): distinguish transport failures from null responses Closes#806 #807
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,155 +1,8 @@ | ||
| const TOOL_LABELS = { | ||
| mem_search: "search", | ||
| mem_save: "save", | ||
| mem_update: "update", | ||
| mem_delete: "delete", | ||
| mem_suggest_topic_key: "suggest topic", | ||
| mem_save_prompt: "save prompt", | ||
| mem_session_summary: "session summary", | ||
| mem_context: "context", | ||
| mem_stats: "stats", | ||
| mem_timeline: "timeline", | ||
| mem_get_observation: "get observation", | ||
| mem_session_start: "start session", | ||
| mem_session_end: "end session", | ||
| mem_current_project: "current project", | ||
| mem_doctor: "doctor", | ||
| mem_capture_passive: "capture passive", | ||
| mem_judge: "judge", | ||
| mem_compare: "compare", | ||
| mem_review: "review", | ||
| }; | ||
|
|
||
| const ARG_KEYS = { | ||
| mem_search: ["query"], | ||
| mem_save: ["title", "type"], | ||
| mem_update: ["id", "title"], | ||
| mem_delete: ["id"], | ||
| mem_suggest_topic_key: ["title", "type"], | ||
| mem_save_prompt: ["content"], | ||
| mem_session_summary: ["content"], | ||
| mem_context: ["project", "scope"], | ||
| mem_stats: ["project"], | ||
| mem_timeline: ["observation_id"], | ||
| mem_get_observation: ["id"], | ||
| mem_session_start: ["id"], | ||
| mem_session_end: ["id"], | ||
| mem_current_project: ["cwd"], | ||
| mem_doctor: ["check", "project"], | ||
| mem_capture_passive: ["source", "content"], | ||
| mem_judge: ["judgment_id", "relation"], | ||
| mem_compare: ["memory_id_a", "memory_id_b"], | ||
| mem_review: ["action", "project", "limit", "observation_id", "id"], | ||
| }; | ||
|
|
||
| export const SUPPORTED_MEMORY_TOOLS = Object.freeze(Object.keys(TOOL_LABELS)); | ||
|
|
||
| export function humanToolName(toolName) { | ||
| return TOOL_LABELS[toolName] ?? toolName.replace(/^mem_/, "").replace(/_/g, " "); | ||
| } | ||
|
|
||
| export function truncateText(value, max = 48) { | ||
| const text = String(value ?? "").replace(/\s+/g, " ").trim(); | ||
| if (text.length <= max) return text; | ||
| return `${text.slice(0, Math.max(0, max - 1))}β¦`; | ||
| } | ||
|
|
||
| function quote(value) { | ||
| const text = truncateText(value); | ||
| return text ? `β${text}β` : ""; | ||
| } | ||
|
|
||
| export function compactToolArg(toolName, args = {}) { | ||
| if (toolName === "mem_review") return compactReviewArg(args); | ||
|
|
||
| const keys = ARG_KEYS[toolName] ?? []; | ||
| for (const key of keys) { | ||
| const value = args?.[key]; | ||
| if (value === undefined || value === null || value === "") continue; | ||
| if (key === "id" || key === "observation_id" || key === "memory_id_a" || key === "memory_id_b") return `#${value}`; | ||
| return quote(value); | ||
| } | ||
| return ""; | ||
| } | ||
|
|
||
| function compactReviewArg(args = {}) { | ||
| const parts = []; | ||
| if (args.action !== undefined && args.action !== null && args.action !== "") parts.push(String(args.action)); | ||
|
|
||
| const id = args.observation_id ?? args.id; | ||
| if (id !== undefined && id !== null && id !== "") parts.push(`#${id}`); | ||
|
|
||
| if (args.project !== undefined && args.project !== null && args.project !== "") parts.push(quote(args.project)); | ||
| if (args.limit !== undefined && args.limit !== null && args.limit !== "") parts.push(`limit ${args.limit}`); | ||
|
|
||
| return parts.join(" "); | ||
| } | ||
|
|
||
| function firstTextContent(result) { | ||
| const block = result?.content?.find?.((entry) => entry?.type === "text" && typeof entry.text === "string"); | ||
| return block?.text ?? ""; | ||
| } | ||
|
|
||
| function resultData(result) { | ||
| return result?.details?.data ?? result?.details ?? result; | ||
| } | ||
|
|
||
| function countItems(value) { | ||
| if (Array.isArray(value)) return value.length; | ||
| if (Array.isArray(value?.results)) return value.results.length; | ||
| if (Array.isArray(value?.observations)) return value.observations.length; | ||
| if (Array.isArray(value?.sessions)) return value.sessions.length; | ||
| if (Array.isArray(value?.prompts)) return value.prompts.length; | ||
| if (typeof value?.count === "number") return value.count; | ||
| return undefined; | ||
| } | ||
|
|
||
| export function compactResultStatus(toolName, result, options = {}) { | ||
| if (options.isPartial) return `${humanToolName(toolName)}β¦`; | ||
| if (options.isError || result?.isError) { | ||
| const text = truncateText(firstTextContent(result) || result?.details?.error || "error", 64); | ||
| return `β ${text}`; | ||
| } | ||
|
|
||
| const data = resultData(result); | ||
| const count = countItems(data); | ||
| if (toolName === "mem_search") return `β ${count ?? 0} result${count === 1 ? "" : "s"}`; | ||
| if (toolName === "mem_context") return `β ${firstTextContent(result) || data?.context ? "loaded" : "empty"}`; | ||
| if (toolName === "mem_stats") return "β loaded"; | ||
| if (toolName === "mem_timeline") return `β ${count ?? "timeline"}`; | ||
| if (toolName === "mem_get_observation") return data?.id ? `β observation #${data.id}` : "β loaded"; | ||
| if (toolName === "mem_save" || toolName === "mem_session_summary") return data?.id ? `β saved #${data.id}` : "β saved"; | ||
| if (toolName === "mem_update") return data?.id ? `β updated #${data.id}` : "β updated"; | ||
| if (toolName === "mem_delete") return data?.id ? `β deleted #${data.id}` : "β deleted"; | ||
| if (toolName === "mem_suggest_topic_key") return data?.topic_key ? `β ${data.topic_key}` : "β suggested"; | ||
| if (toolName === "mem_save_prompt") return data?.id ? `β prompt #${data.id}` : "β prompt saved"; | ||
| if (toolName === "mem_session_start") return "β started"; | ||
| if (toolName === "mem_session_end") return "β ended"; | ||
| if (toolName === "mem_current_project") return data?.project ? `β ${data.project}` : "β detected"; | ||
| if (toolName === "mem_doctor") return data?.status ? `β ${data.status}` : "β checked"; | ||
| if (toolName === "mem_capture_passive") return `β captured ${data?.saved ?? count ?? 0}`; | ||
| if (toolName === "mem_judge") return data?.relation?.sync_id ? `β judged ${data.relation.sync_id}` : "β judged"; | ||
| if (toolName === "mem_compare") return data?.sync_id ? `β ${data.sync_id}` : "β compared"; | ||
| if (toolName === "mem_review") { | ||
| if (count !== undefined) return `β ${count} need${count === 1 ? "s" : ""} review`; | ||
| const id = data?.id ?? data?.observation_id ?? data?.observation?.id; | ||
| return id ? `β reviewed #${id}` : "β reviewed"; | ||
| } | ||
| return "β done"; | ||
| } | ||
|
|
||
| export function renderCallText(toolName, args = {}) { | ||
| const arg = compactToolArg(toolName, args); | ||
| return `π§ ${humanToolName(toolName)}${arg ? ` ${arg}` : ""} β¦`; | ||
| } | ||
|
|
||
| export function renderResultText(toolName, result, options = {}) { | ||
| const status = compactResultStatus(toolName, result, options); | ||
| if (!options.expanded || options.isPartial) return `β³ ${status}`; | ||
|
|
||
| const text = firstTextContent(result); | ||
| if (text) return `β³ ${status}\n\n${text}`; | ||
|
|
||
| const data = resultData(result); | ||
| return `β³ ${status}\n\n${truncateText(JSON.stringify(data, null, 2), 2000)}`; | ||
| } | ||
| // Compact UI chrome for Engram memory tools β a compatibility facade. | ||
| // Tool labels, result status lines, and their shared text helpers live in | ||
| // memory-tool-status.js; call-argument formatting and the call/result renderers | ||
| // live in memory-tool-render.js. Everything is re-exported here so existing | ||
| // consumers (index.ts and the test suite) keep importing from one module. | ||
|
|
||
| export { SUPPORTED_MEMORY_TOOLS, compactResultStatus, humanToolName } from "./memory-tool-status.js"; | ||
| export { compactToolArg, renderCallText, renderResultText } from "./memory-tool-render.js"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| // Compact call rendering for Engram memory tools: turns a tool call's arguments | ||
| // into the short argument chip shown on the call line, and renders the call and | ||
| // result lines displayed in the Pi status area. Tool labels and result status | ||
| // lines live in memory-tool-status.js; memory-tool-chrome.js re-exports this | ||
| // module's public surface so existing consumers keep importing from one place. | ||
|
|
||
| import { compactResultStatus, firstTextContent, humanToolName, resultData, truncateText } from "./memory-tool-status.js"; | ||
|
|
||
| const ARG_KEYS = { | ||
| mem_search: ["query"], | ||
| mem_save: ["title", "type"], | ||
| mem_update: ["id", "title"], | ||
| mem_delete: ["id"], | ||
| mem_suggest_topic_key: ["title", "type"], | ||
| mem_save_prompt: ["content"], | ||
| mem_session_summary: ["content"], | ||
| mem_context: ["project", "scope"], | ||
| mem_stats: ["project"], | ||
| mem_timeline: ["observation_id"], | ||
| mem_get_observation: ["id"], | ||
| mem_session_start: ["id"], | ||
| mem_session_end: ["id"], | ||
| mem_current_project: ["cwd"], | ||
| mem_doctor: ["check", "project"], | ||
| mem_capture_passive: ["source", "content"], | ||
| mem_judge: ["judgment_id", "relation"], | ||
| mem_compare: ["memory_id_a", "memory_id_b"], | ||
| mem_review: ["action", "project", "limit", "observation_id", "id"], | ||
| }; | ||
|
|
||
| // Argument keys whose values read best as a bare id reference (#42). | ||
| const ID_ARG_KEYS = new Set(["id", "observation_id", "memory_id_a", "memory_id_b"]); | ||
|
|
||
| // An argument is shown only when the caller actually provided one. | ||
| function hasValue(value) { | ||
| return value !== undefined && value !== null && value !== ""; | ||
| } | ||
|
|
||
| function quote(value) { | ||
| const text = truncateText(value); | ||
| return text ? `β${text}β` : ""; | ||
| } | ||
|
|
||
| export function compactToolArg(toolName, args = {}) { | ||
| if (toolName === "mem_review") return compactReviewArg(args); | ||
|
|
||
| const keys = ARG_KEYS[toolName] ?? []; | ||
| for (const key of keys) { | ||
| const value = args?.[key]; | ||
| if (!hasValue(value)) continue; | ||
| if (ID_ARG_KEYS.has(key)) return `#${value}`; | ||
| return quote(value); | ||
| } | ||
| return ""; | ||
| } | ||
|
|
||
| function compactReviewArg(args = {}) { | ||
| const parts = []; | ||
| if (hasValue(args.action)) parts.push(String(args.action)); | ||
|
|
||
| const id = args.observation_id ?? args.id; | ||
| if (hasValue(id)) parts.push(`#${id}`); | ||
|
|
||
| if (hasValue(args.project)) parts.push(quote(args.project)); | ||
| if (hasValue(args.limit)) parts.push(`limit ${args.limit}`); | ||
|
|
||
| return parts.join(" "); | ||
| } | ||
|
|
||
| export function renderCallText(toolName, args = {}) { | ||
| const arg = compactToolArg(toolName, args); | ||
| return `π§ ${humanToolName(toolName)}${arg ? ` ${arg}` : ""} β¦`; | ||
| } | ||
|
|
||
| export function renderResultText(toolName, result, options = {}) { | ||
| const status = compactResultStatus(toolName, result, options); | ||
| if (!options.expanded || options.isPartial) return `β³ ${status}`; | ||
|
|
||
| const text = firstTextContent(result); | ||
| if (text) return `β³ ${status}\n\n${text}`; | ||
|
|
||
| const data = resultData(result); | ||
| return `β³ ${status}\n\n${truncateText(JSON.stringify(data, null, 2), 2000)}`; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||||||||||||||||
| // Compact status presentation for Engram memory tools: human tool labels, text | ||||||||||||||||||||
| // truncation, and the per-tool result status lines. memory-tool-render.js keeps | ||||||||||||||||||||
| // the call-argument formatting and call/result renderers; memory-tool-chrome.js | ||||||||||||||||||||
| // re-exports both modules' public surface so existing consumers keep importing | ||||||||||||||||||||
| // from one place. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const TOOL_LABELS = { | ||||||||||||||||||||
| mem_search: "search", | ||||||||||||||||||||
| mem_save: "save", | ||||||||||||||||||||
| mem_update: "update", | ||||||||||||||||||||
| mem_delete: "delete", | ||||||||||||||||||||
| mem_suggest_topic_key: "suggest topic", | ||||||||||||||||||||
| mem_save_prompt: "save prompt", | ||||||||||||||||||||
| mem_session_summary: "session summary", | ||||||||||||||||||||
| mem_context: "context", | ||||||||||||||||||||
| mem_stats: "stats", | ||||||||||||||||||||
| mem_timeline: "timeline", | ||||||||||||||||||||
| mem_get_observation: "get observation", | ||||||||||||||||||||
| mem_session_start: "start session", | ||||||||||||||||||||
| mem_session_end: "end session", | ||||||||||||||||||||
| mem_current_project: "current project", | ||||||||||||||||||||
| mem_doctor: "doctor", | ||||||||||||||||||||
| mem_capture_passive: "capture passive", | ||||||||||||||||||||
| mem_judge: "judge", | ||||||||||||||||||||
| mem_compare: "compare", | ||||||||||||||||||||
| mem_review: "review", | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export const SUPPORTED_MEMORY_TOOLS = Object.freeze(Object.keys(TOOL_LABELS)); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function humanToolName(toolName) { | ||||||||||||||||||||
| return TOOL_LABELS[toolName] ?? toolName.replace(/^mem_/, "").replace(/_/g, " "); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function truncateText(value, max = 48) { | ||||||||||||||||||||
| const text = String(value ?? "").replace(/\s+/g, " ").trim(); | ||||||||||||||||||||
| if (text.length <= max) return text; | ||||||||||||||||||||
| return `${text.slice(0, Math.max(0, max - 1))}β¦`; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function firstTextContent(result) { | ||||||||||||||||||||
| const block = result?.content?.find?.((entry) => entry?.type === "text" && typeof entry.text === "string"); | ||||||||||||||||||||
| return block?.text ?? ""; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function resultData(result) { | ||||||||||||||||||||
| return result?.details?.data ?? result?.details ?? result; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+46
to
+48
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. π― Functional Correctness | π‘ Minor | β‘ Quick win Preserve Line 47 treats Proposed fix export function resultData(result) {
- return result?.details?.data ?? result?.details ?? result;
+ if (result?.details && Object.prototype.hasOwnProperty.call(result.details, "data")) {
+ return result.details.data;
+ }
+ return result?.details ?? result;
}π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| // Envelope shapes that carry a countable item list, in resolution order. | ||||||||||||||||||||
| const COUNT_FIELDS = ["results", "observations", "sessions", "prompts"]; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function countItems(value) { | ||||||||||||||||||||
| if (Array.isArray(value)) return value.length; | ||||||||||||||||||||
| for (const field of COUNT_FIELDS) { | ||||||||||||||||||||
| if (Array.isArray(value?.[field])) return value[field].length; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (typeof value?.count === "number") return value.count; | ||||||||||||||||||||
| return undefined; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function partialStatus(toolName) { | ||||||||||||||||||||
| return `${humanToolName(toolName)}β¦`; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function errorStatus(result) { | ||||||||||||||||||||
| const text = truncateText(firstTextContent(result) || result?.details?.error || "error", 64); | ||||||||||||||||||||
| return `β ${text}`; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Saved-by-id shapes are shared by mem_save and mem_session_summary. | ||||||||||||||||||||
| function savedStatus({ data }) { | ||||||||||||||||||||
| return data?.id ? `β saved #${data.id}` : "β saved"; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function reviewStatus({ data, count }) { | ||||||||||||||||||||
| if (count !== undefined) return `β ${count} need${count === 1 ? "s" : ""} review`; | ||||||||||||||||||||
| const id = data?.id ?? data?.observation_id ?? data?.observation?.id; | ||||||||||||||||||||
| return id ? `β reviewed #${id}` : "β reviewed"; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // One small formatter per tool: each receives { data, count, result } and returns the exact | ||||||||||||||||||||
| // success line that tool previously produced inline in compactResultStatus. | ||||||||||||||||||||
| const RESULT_STATUS_FORMATTERS = { | ||||||||||||||||||||
| mem_search: ({ count }) => `β ${count ?? 0} result${count === 1 ? "" : "s"}`, | ||||||||||||||||||||
| mem_context: ({ data, result }) => `β ${firstTextContent(result) || data?.context ? "loaded" : "empty"}`, | ||||||||||||||||||||
| mem_stats: () => "β loaded", | ||||||||||||||||||||
| mem_timeline: ({ count }) => `β ${count ?? "timeline"}`, | ||||||||||||||||||||
| mem_get_observation: ({ data }) => (data?.id ? `β observation #${data.id}` : "β loaded"), | ||||||||||||||||||||
| mem_save: savedStatus, | ||||||||||||||||||||
| mem_session_summary: savedStatus, | ||||||||||||||||||||
| mem_update: ({ data }) => (data?.id ? `β updated #${data.id}` : "β updated"), | ||||||||||||||||||||
| mem_delete: ({ data }) => (data?.id ? `β deleted #${data.id}` : "β deleted"), | ||||||||||||||||||||
| mem_suggest_topic_key: ({ data }) => (data?.topic_key ? `β ${data.topic_key}` : "β suggested"), | ||||||||||||||||||||
| mem_save_prompt: ({ data }) => (data?.id ? `β prompt #${data.id}` : "β prompt saved"), | ||||||||||||||||||||
| mem_session_start: () => "β started", | ||||||||||||||||||||
| mem_session_end: () => "β ended", | ||||||||||||||||||||
| mem_current_project: ({ data }) => (data?.project ? `β ${data.project}` : "β detected"), | ||||||||||||||||||||
| mem_doctor: ({ data }) => (data?.status ? `β ${data.status}` : "β checked"), | ||||||||||||||||||||
| mem_capture_passive: ({ data, count }) => `β captured ${data?.saved ?? count ?? 0}`, | ||||||||||||||||||||
| mem_judge: ({ data }) => (data?.relation?.sync_id ? `β judged ${data.relation.sync_id}` : "β judged"), | ||||||||||||||||||||
| mem_compare: ({ data }) => (data?.sync_id ? `β ${data.sync_id}` : "β compared"), | ||||||||||||||||||||
| mem_review: reviewStatus, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function successStatus(toolName, result) { | ||||||||||||||||||||
| const formatter = RESULT_STATUS_FORMATTERS[toolName]; | ||||||||||||||||||||
| // Unknown tools (and prototype-chain keys) keep the generic fallback. | ||||||||||||||||||||
| if (typeof formatter !== "function") return "β done"; | ||||||||||||||||||||
| const data = resultData(result); | ||||||||||||||||||||
| return formatter({ data, count: countItems(data), result }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| export function compactResultStatus(toolName, result, options = {}) { | ||||||||||||||||||||
| if (options.isPartial) return partialStatus(toolName); | ||||||||||||||||||||
| if (options.isError || result?.isError) return errorStatus(result); | ||||||||||||||||||||
| return successStatus(toolName, result); | ||||||||||||||||||||
| } | ||||||||||||||||||||
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.
π― Functional Correctness | π‘ Minor | β‘ Quick win
Restrict tool configuration lookups to own keys.
The maps use direct property access.
renderCallText("toString", {})reaches inheritedARG_KEYS.toString, andcompactToolArgthen attempts to iterate it. This throws instead of using the unknown-tool fallback. The status maps have the same problem.plugin/pi/memory-tool-status.js#L31-L32: use an own-key lookup before readingTOOL_LABELS.plugin/pi/memory-tool-status.js#L106-L109: use an own-key lookup before readingRESULT_STATUS_FORMATTERS.plugin/pi/memory-tool-render.js#L47-L48: use an own-key lookup before readingARG_KEYS.π Affects 2 files
plugin/pi/memory-tool-status.js#L31-L32(this comment)plugin/pi/memory-tool-status.js#L106-L109plugin/pi/memory-tool-render.js#L47-L48π€ Prompt for AI Agents