@sage-protocol/pi-adapter 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,22 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { spawn } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
+ const SAGE_ENV_PASSTHROUGH = [
5
+ "SAGE_FILTER_EXECUTION",
6
+ "SAGE_SKILL_USED",
7
+ "SAGE_SKILL_OUTCOME",
8
+ "SAGE_SOURCE",
9
+ "SAGE_SESSION_ID",
10
+ ];
11
+ function sagePassthroughEnv() {
12
+ const env = {};
13
+ for (const key of SAGE_ENV_PASSTHROUGH) {
14
+ const value = process.env[key];
15
+ if (value !== undefined)
16
+ env[key] = value;
17
+ }
18
+ return env;
19
+ }
4
20
  function formatSageCommand(command, args) {
5
21
  const quoted = [command, ...args].map((part) => {
6
22
  if (!part)
@@ -582,6 +598,10 @@ export class SageMcpBridge {
582
598
  stopped = false;
583
599
  startPromise = null;
584
600
  stderrBuffer = "";
601
+ degraded = false;
602
+ lastHealthError = null;
603
+ lastHealthCheckedAt = null;
604
+ cachedToolNames = [];
585
605
  constructor(command, args, env = {}, requestTimeoutMs = 20_000) {
586
606
  this.command = command;
587
607
  this.args = args;
@@ -591,6 +611,19 @@ export class SageMcpBridge {
591
611
  isReady() {
592
612
  return this.ready && this.proc !== null && !this.stopped;
593
613
  }
614
+ isDegraded() {
615
+ return this.degraded;
616
+ }
617
+ getHealthSnapshot() {
618
+ return {
619
+ bridgeConnected: this.isReady(),
620
+ degraded: this.degraded,
621
+ lastHealthError: this.lastHealthError,
622
+ lastHealthCheckedAt: this.lastHealthCheckedAt,
623
+ availableToolCount: this.cachedToolNames.length,
624
+ availableToolNames: [...this.cachedToolNames],
625
+ };
626
+ }
594
627
  async start() {
595
628
  if (this.isReady())
596
629
  return;
@@ -600,6 +633,10 @@ export class SageMcpBridge {
600
633
  this.stopped = false;
601
634
  await this.spawn();
602
635
  await this.initialize();
636
+ // Fail-open health probe: mark degraded if tools/list fails post-init,
637
+ // but never throw from start(). Tool-use call sites surface the
638
+ // structured degraded error when the bridge is unreachable.
639
+ await this.probeHealth(Math.min(this.requestTimeoutMs, 2_000));
603
640
  })();
604
641
  try {
605
642
  await this.startPromise;
@@ -608,6 +645,22 @@ export class SageMcpBridge {
608
645
  this.startPromise = null;
609
646
  }
610
647
  }
648
+ async probeHealth(timeoutMs = Math.min(this.requestTimeoutMs, 2_000)) {
649
+ this.lastHealthCheckedAt = Date.now();
650
+ try {
651
+ const tools = await this.listTools(timeoutMs);
652
+ this.cachedToolNames = tools.map((t) => t.name).filter(Boolean);
653
+ this.degraded = false;
654
+ this.lastHealthError = null;
655
+ return true;
656
+ }
657
+ catch (error) {
658
+ this.degraded = true;
659
+ this.lastHealthError =
660
+ error instanceof Error ? error.message : String(error);
661
+ return false;
662
+ }
663
+ }
611
664
  async stop() {
612
665
  this.stopped = true;
613
666
  this.ready = false;
@@ -617,8 +670,8 @@ export class SageMcpBridge {
617
670
  this.proc = null;
618
671
  }
619
672
  }
620
- async listTools() {
621
- const result = (await this.request("tools/list", {}));
673
+ async listTools(timeoutMs) {
674
+ const result = (await this.request("tools/list", {}, timeoutMs));
622
675
  return result.tools ?? [];
623
676
  }
624
677
  async callTool(name, args) {
@@ -636,10 +689,45 @@ export class SageMcpBridge {
636
689
  return result;
637
690
  }
638
691
  async callCodeModeSearch(args) {
639
- return this.callWithFallback(CODE_MODE_SEARCH_TOOLS, args);
692
+ this.assertReadyForToolCall();
693
+ try {
694
+ return await this.callWithFallback(CODE_MODE_SEARCH_TOOLS, args);
695
+ }
696
+ catch (error) {
697
+ this.markDegradedFromError(error);
698
+ throw error;
699
+ }
640
700
  }
641
701
  async callCodeModeExecute(args) {
642
- return this.callWithFallback(CODE_MODE_EXECUTE_TOOLS, args);
702
+ this.assertReadyForToolCall();
703
+ try {
704
+ return await this.callWithFallback(CODE_MODE_EXECUTE_TOOLS, args);
705
+ }
706
+ catch (error) {
707
+ this.markDegradedFromError(error);
708
+ throw error;
709
+ }
710
+ }
711
+ assertReadyForToolCall() {
712
+ if (!this.isReady() || this.degraded) {
713
+ const detail = this.lastHealthError
714
+ ? ` Last health error: ${this.lastHealthError}.`
715
+ : "";
716
+ throw new Error(`Sage MCP bridge unavailable; restart Pi or verify 'sage mcp start' works.${detail}`);
717
+ }
718
+ }
719
+ markDegradedFromError(error) {
720
+ const message = error instanceof Error ? error.message : String(error);
721
+ // Only flip degraded on transport-level failures, not on known MCP-level
722
+ // errors (for example `tools/list` returning an unknown tool). Transport
723
+ // failures surface as "not running", "stopped", or plain JSON-RPC errors
724
+ // from dead processes.
725
+ const looksTransportFailure = /bridge stopped|not running|failed to spawn|request timed out|EPIPE|ECONNRESET|closed/i.test(message);
726
+ if (!looksTransportFailure)
727
+ return;
728
+ this.degraded = true;
729
+ this.lastHealthError = message;
730
+ this.lastHealthCheckedAt = Date.now();
643
731
  }
644
732
  async callWithFallback(candidates, args) {
645
733
  let lastUnknownError = null;
@@ -714,7 +802,7 @@ export class SageMcpBridge {
714
802
  resolve();
715
803
  });
716
804
  }
717
- request(method, params) {
805
+ request(method, params, timeoutMs = this.requestTimeoutMs) {
718
806
  return new Promise((resolve, reject) => {
719
807
  if (!this.proc?.stdin?.writable) {
720
808
  reject(new Error("MCP process not running"));
@@ -724,7 +812,7 @@ export class SageMcpBridge {
724
812
  const timeout = setTimeout(() => {
725
813
  this.pending.delete(id);
726
814
  reject(new Error(this.withRecentStderr(`MCP request timed out: ${method}`)));
727
- }, this.requestTimeoutMs);
815
+ }, timeoutMs);
728
816
  this.pending.set(id, { resolve, reject, timeout });
729
817
  const req = { jsonrpc: "2.0", id, method, params };
730
818
  this.proc.stdin.write(JSON.stringify(req) + "\n", (err) => {
@@ -793,105 +881,89 @@ export class SageHookRuntime {
793
881
  this.commandRunner = config.commandRunner ?? defaultSageCommandRunner;
794
882
  }
795
883
  async getSessionContext(sessionId) {
796
- const result = await this.run(["skill", "context", "--format", "opencode"], { SAGE_SESSION_ID: sessionId ?? "" }, undefined, true);
884
+ const result = await this.run(["skill", "context", "--format", "pi"], { SAGE_SESSION_ID: sessionId ?? "" }, undefined, true);
797
885
  return result?.stdout.trim() ?? "";
798
886
  }
799
887
  /**
800
- * Gather dynamic Sage Protocol identity context for system prompt injection.
888
+ * Gather skill visibility summary for user-facing display.
801
889
  *
802
- * Fetches wallet, library, and skill information in parallel and returns a
803
- * compact block suitable for appending to the system prompt. Returns an empty
804
- * string if the sage CLI is unavailable or all queries fail.
805
- *
806
- * The result is cached for `identityCacheTtlMs` (default 60 s) to avoid
807
- * redundant CLI calls on every turn.
890
+ * Runs `sage library list` and `sage library active` in parallel to
891
+ * produce a summary of what libraries, skills, and prompts are loaded.
892
+ * Returns empty string when the CLI is unavailable.
808
893
  */
809
- identityCache = null;
810
- identityCacheTtlMs = 60_000;
811
- async getIdentityContext() {
812
- const now = Date.now();
813
- if (this.identityCache && now < this.identityCache.expiresAt) {
814
- return this.identityCache.value;
815
- }
816
- const [walletResult, libraryResult, activeResult] = await Promise.allSettled([
817
- this.run(["wallet", "current"], {}, undefined, true),
894
+ async getSkillVisibilitySummary() {
895
+ const [libraryResult, activeResult] = await Promise.allSettled([
818
896
  this.run(["library", "list"], {}, undefined, true),
819
897
  this.run(["library", "active"], {}, undefined, true),
820
898
  ]);
821
- const lines = [];
822
- // --- Wallet ---
823
- const walletOut = walletResult.status === "fulfilled" ? walletResult.value?.stdout.trim() ?? "" : "";
824
- if (walletOut) {
825
- const addrMatch = walletOut.match(/Address:\s*(0x[a-fA-F0-9]+)/i);
826
- const typeMatch = walletOut.match(/Type:\s*(\S+)/i);
827
- const delegationMatch = walletOut.match(/Active on-chain delegation:\s*(.+)/i);
828
- const delegatorMatch = walletOut.match(/Delegator:\s*(0x[a-fA-F0-9]+)/i);
829
- const delegateSignerMatch = walletOut.match(/Delegate signer:\s*(0x[a-fA-F0-9]+)/i);
830
- const profileMatch = walletOut.match(/Profile:\s*(\S+)/i);
831
- const networkMatch = walletOut.match(/Network:\s*(.+)/i);
832
- const chainMatch = walletOut.match(/Chain(?:\s*ID)?:\s*(\S+)/i);
833
- if (addrMatch) {
834
- const addr = addrMatch[1];
835
- const walletType = typeMatch?.[1] ?? "unknown";
836
- const network = networkMatch?.[1]?.trim() ?? (chainMatch?.[1] === "8453" ? "Base Mainnet" : chainMatch?.[1] === "84532" ? "Base Sepolia" : chainMatch?.[1] ?? "");
837
- lines.push(`- Wallet: ${addr.slice(0, 10)}...${addr.slice(-4)} (${walletType}${network ? `, ${network}` : ""})`);
838
- }
839
- if (delegationMatch && delegatorMatch && delegateSignerMatch) {
840
- const delegator = delegatorMatch[1];
841
- const delegate = delegateSignerMatch[1];
842
- lines.push(`- On-chain delegation: ${delegationMatch[1].trim()} via ${delegate.slice(0, 10)}...${delegate.slice(-4)} for ${delegator.slice(0, 10)}...${delegator.slice(-4)}`);
843
- }
844
- }
845
- // --- Counts only — agent can query details via tools ---
846
- const activeOut = activeResult.status === "fulfilled" ? activeResult.value?.stdout.trim() ?? "" : "";
847
- if (activeOut) {
848
- let activeCount = 0;
849
- for (const line of activeOut.split("\n")) {
850
- if (/^\s*\d+\.\s+/.test(line))
851
- activeCount++;
852
- }
853
- if (activeCount > 0) {
854
- lines.push(`- ${activeCount} active libraries`);
855
- }
856
- }
899
+ const libraries = [];
857
900
  const libraryOut = libraryResult.status === "fulfilled" ? libraryResult.value?.stdout.trim() ?? "" : "";
858
901
  if (libraryOut) {
859
- let totalSkills = 0;
860
- let totalPrompts = 0;
861
- let libraryCount = 0;
862
902
  for (const line of libraryOut.split("\n")) {
863
- const m = line.match(/\((\d+)\s+prompts?,\s*(\d+)\s+skills?\)/);
903
+ const m = line.match(/^\s*(.+?)\s+\((\d+)\s+prompts?,\s*(\d+)\s+skills?\)/);
864
904
  if (m) {
865
- libraryCount++;
866
- totalPrompts += Number.parseInt(m[1], 10);
867
- totalSkills += Number.parseInt(m[2], 10);
905
+ libraries.push({
906
+ name: m[1].trim(),
907
+ prompts: Number.parseInt(m[2], 10),
908
+ skills: Number.parseInt(m[3], 10),
909
+ });
868
910
  }
869
911
  }
870
- if (libraryCount > 0) {
871
- lines.push(`- ${libraryCount} libraries, ${totalSkills} skills, ${totalPrompts} prompts installed`);
912
+ }
913
+ let totalSkills = libraries.reduce((s, l) => s + l.skills, 0);
914
+ let totalPrompts = libraries.reduce((s, l) => s + l.prompts, 0);
915
+ // --- Active libraries + exposed skills ---
916
+ const activeOut = activeResult.status === "fulfilled" ? activeResult.value?.stdout.trim() ?? "" : "";
917
+ const activeNames = [];
918
+ const exposedSkills = [];
919
+ let inSkillSection = false;
920
+ if (activeOut) {
921
+ for (const line of activeOut.split("\n")) {
922
+ if (line.includes("Skill-activated libraries")) {
923
+ inSkillSection = true;
924
+ continue;
925
+ }
926
+ if (inSkillSection) {
927
+ const skillMatch = line.match(/^\s+-\s+(\S+)/);
928
+ if (skillMatch) {
929
+ exposedSkills.push(skillMatch[1]);
930
+ }
931
+ continue;
932
+ }
933
+ // Prompt-active section: numbered list " 1. name (id)"
934
+ const numMatch = line.match(/^\s*\d+\.\s+(\S+)/);
935
+ if (numMatch) {
936
+ activeNames.push(numMatch[1]);
937
+ }
872
938
  }
873
939
  }
874
- const PROTOCOL_DESC = "Sage Protocol is a decentralized network for curating prompts, skills, and knowledge on Base (L2).\n" +
875
- "Libraries are like gardens — cultivated by communities around shared interests, taste, and tooling preferences.\n" +
876
- "Users follow creators, tip valuable work, and post bounties for skills they need.\n" +
877
- "Skills stay valuable when kept current with tooling and documentation — different knowledge bases and context work together to provide compounding value.\n" +
878
- "SXXX is the governance token: vote, create DAOs, stake on proposals, shape the protocol. Burns from activity create deflationary pressure.\n" +
879
- "Credits are the utility currency for IPFS storage and API services.\n" +
880
- "The more skills published and curated, the more valuable discovery becomes for every user and agent — a compounding network effect.\n" +
881
- "Use sage_search, sage_execute, and sage_status to interact with the protocol.";
882
- const KEY_COMMANDS = "### Key Commands\n" +
883
- "- Search: `sage_search({ domain: \"skills\", action: \"search\", params: { query: \"...\" } })` or `sage search \"...\" --search-type skills`\n" +
884
- "- Use skill: `sage_execute({ domain: \"skills\", action: \"use\", params: { key: \"...\" } })`\n" +
885
- "- Tip: `sage tip <address> <amount>` or `sage social tip ...`\n" +
886
- "- Bounty: `sage bounties create --title \"...\" --reward <amount>`\n" +
887
- "- DAOs: `sage governance dao discover`\n" +
888
- "- Publish: `sage library push <name>`\n" +
889
- "- Follow: `sage social follow <address>`";
890
- const block = lines.length > 0
891
- ? `## Sage Protocol Context\n${PROTOCOL_DESC}\n\n${lines.join("\n")}\n\n${KEY_COMMANDS}`
892
- : "";
893
- this.identityCache = { value: block, expiresAt: now + this.identityCacheTtlMs };
894
- return block;
940
+ // --- Build display ---
941
+ const headerParts = [];
942
+ if (libraries.length > 0) {
943
+ headerParts.push(`${libraries.length} libraries`);
944
+ }
945
+ if (totalSkills > 0) {
946
+ headerParts.push(`${totalSkills} skills`);
947
+ }
948
+ if (totalPrompts > 0) {
949
+ headerParts.push(`${totalPrompts} prompts`);
950
+ }
951
+ const header = headerParts.length > 0 ? `[Sage] ${headerParts.join(", ")} loaded` : "";
952
+ const detailParts = [];
953
+ if (activeNames.length > 0) {
954
+ const activeDetails = activeNames.map((name) => {
955
+ const lib = libraries.find((l) => l.name === name);
956
+ return lib ? `${name} (${lib.skills}s, ${lib.prompts}p)` : name;
957
+ });
958
+ detailParts.push(`active: ${activeDetails.join(", ")}`);
959
+ }
960
+ if (exposedSkills.length > 0) {
961
+ detailParts.push(`exposed: ${exposedSkills.slice(0, 6).join(", ")}${exposedSkills.length > 6 ? ` +${exposedSkills.length - 6} more` : ""}`);
962
+ }
963
+ if (header && detailParts.length > 0) {
964
+ return `${header}\n${detailParts.join(" | ")}`;
965
+ }
966
+ return header;
895
967
  }
896
968
  async suggestSkill(prompt, sessionId) {
897
969
  const trimmed = prompt.trim();
@@ -936,6 +1008,32 @@ export class SageHookRuntime {
936
1008
  TOKENS_OUTPUT: usage.output ?? "",
937
1009
  }, JSON.stringify({ response: trimmed }), true);
938
1010
  }
1011
+ async recordSuggestionUse(skillKeys, sessionId) {
1012
+ if (this.config.enableRlmFeedback === false)
1013
+ return;
1014
+ const used = [...new Set(skillKeys.map((key) => key.trim()).filter(Boolean))];
1015
+ if (used.length === 0)
1016
+ return;
1017
+ await this.run(["suggest", "feedback", "use", ...used.flatMap((key) => ["--used", key])], {
1018
+ SAGE_SOURCE: this.config.source ?? "pi",
1019
+ SAGE_SESSION_ID: sessionId ?? "",
1020
+ }, undefined, true);
1021
+ }
1022
+ async recordExecutionOutcome(skillKey, status, queryPreview, sessionId) {
1023
+ if (this.config.enableRlmFeedback === false)
1024
+ return;
1025
+ const key = skillKey.trim();
1026
+ if (!key)
1027
+ return;
1028
+ const args = ["suggest", "feedback", "outcome", key, "--status", status];
1029
+ const preview = queryPreview?.trim();
1030
+ if (preview)
1031
+ args.push("--query-preview", preview.slice(0, 240));
1032
+ await this.run(args, {
1033
+ SAGE_SOURCE: this.config.source ?? "pi",
1034
+ SAGE_SESSION_ID: sessionId ?? "",
1035
+ }, undefined, true);
1036
+ }
939
1037
  async scanPreTool(toolName, toolInput, sessionId) {
940
1038
  if (this.config.enableSecurityHooks === false)
941
1039
  return null;
@@ -992,6 +1090,7 @@ export class SageHookRuntime {
992
1090
  const commandEnv = {
993
1091
  HOME: process.env.HOME ?? "",
994
1092
  PATH: process.env.PATH ?? "",
1093
+ ...sagePassthroughEnv(),
995
1094
  ...(this.config.env ?? {}),
996
1095
  ...env,
997
1096
  };
@@ -1188,39 +1287,21 @@ export function createSageExecuteTool(mcpBridge) {
1188
1287
  },
1189
1288
  };
1190
1289
  }
1191
- // Tier 3: Complete CLI reference — available on-demand via sage_status().
1192
- // Not injected into system prompt; model requests it when needed.
1193
1290
  const SAGE_CLI_REFERENCE = `
1194
- ## Sage CLI Reference (Tier 3 — on-demand)
1195
-
1196
- ### Skill Trigger Workflow
1197
- 1) Discover: sage_search({ domain: "skills", action: "search", params: { query: "..." } })
1198
- 2) Inspect: sage_search({ domain: "skills", action: "get", params: { key: "..." } })
1199
- 3) Activate: sage_execute({ domain: "skills", action: "use", params: { key: "..." } })
1200
- 4) Hook mode: sage suggest hook skill --limit 3 --provision (reads PROMPT env/stdin)
1291
+ ## Sage CLI Reference (on-demand)
1201
1292
 
1202
- ### Rust CLI Fallback (when user asks for terminal commands)
1203
- - Discovery: sage search "<query>" --search-type skills --scope both
1204
- - Suggest: sage suggest skill "<intent>" --limit 3 --provision
1205
- - Inspect: sage skill get <key>
1206
- - Libraries: sage library list | sage library discover | sage library sync
1207
- - Gov: sage governance dao discover | sage governance proposals list --dao <addr>
1208
- - MCP: sage mcp start | sage mcp hub list | sage mcp hub status
1293
+ ### Skill Discovery
1294
+ - sage_search({ domain: "skills", action: "search", params: { query: "..." } })
1295
+ - sage_search({ domain: "skills", action: "get", params: { key: "..." } })
1209
1296
 
1210
- ### Library / DAO / Bounty Recipes
1211
- - Library: sage library create <name>; sage library skill add <path> -l <library>; sage library prompt add <name> -l <library> --file <path>; sage library push <library>; sage library promote <library> --dao 0x... --collection default
1212
- - DAO: sage governance dao create --name "My DAO" --description "..." --governance personal|team|community [--operator 0x...|--burn 1500]
1213
- - Bounty: sage bounties create --title "Task" --description "..." --reward 100 --deadline 7d --subdao 0x...
1297
+ ### External Tools
1298
+ - Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
1299
+ - URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
1300
+ - List tools: sage_search({ domain: "external", action: "list_servers" })
1214
1301
 
1215
- ### Hub Management
1216
- - Start: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
1217
- - Stop: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
1218
- - List: sage_search({ domain: "external", action: "list_servers" })
1219
-
1220
- ### Guardrails
1221
- - Verify uncertain syntax with sage --help / sage <group> --help.
1222
- - Prefer sage search --search-type skills when sage skill search is unavailable.
1223
- - Do not use legacy removed top-level commands: sage dao ..., sage proposals ..., sage ipfs ...
1302
+ ### Hub
1303
+ - Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
1304
+ - Stop server: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
1224
1305
  `.trim();
1225
1306
  export function createSageStatusTool(mcpBridge) {
1226
1307
  return {
@@ -1231,7 +1312,8 @@ export function createSageStatusTool(mcpBridge) {
1231
1312
  "Call this to see what external tools (web search, GitHub, fetch, browser automation, etc.) are available.",
1232
1313
  parameters: Type.Object({}),
1233
1314
  execute: async () => {
1234
- const bridgeConnected = mcpBridge.isReady();
1315
+ const health = mcpBridge.getHealthSnapshot();
1316
+ const bridgeConnected = health.bridgeConnected && !health.degraded;
1235
1317
  if (bridgeConnected) {
1236
1318
  try {
1237
1319
  const context = await mcpBridge.callCodeModeSearch({
@@ -1245,7 +1327,10 @@ export function createSageStatusTool(mcpBridge) {
1245
1327
  type: "text",
1246
1328
  text: JSON.stringify({
1247
1329
  bridgeConnected,
1330
+ degraded: false,
1248
1331
  sageAdapterVersion: "0.2.0",
1332
+ availableToolCount: health.availableToolCount,
1333
+ availableToolNames: health.availableToolNames,
1249
1334
  context: extractJsonFromMcpResult(context) ?? context,
1250
1335
  }, null, 2),
1251
1336
  },
@@ -1256,8 +1341,27 @@ export function createSageStatusTool(mcpBridge) {
1256
1341
  ],
1257
1342
  };
1258
1343
  }
1259
- catch {
1260
- // no-op, fallback below
1344
+ catch (error) {
1345
+ // Tool call failed even though the bridge claims ready; surface as
1346
+ // degraded rather than falling back silently.
1347
+ return {
1348
+ content: [
1349
+ {
1350
+ type: "text",
1351
+ text: JSON.stringify({
1352
+ bridgeConnected,
1353
+ degraded: true,
1354
+ sageAdapterVersion: "0.2.0",
1355
+ lastHealthError: error instanceof Error ? error.message : String(error),
1356
+ }, null, 2),
1357
+ },
1358
+ {
1359
+ type: "text",
1360
+ text: SAGE_CLI_REFERENCE,
1361
+ },
1362
+ ],
1363
+ details: { bridgeConnected, degraded: true },
1364
+ };
1261
1365
  }
1262
1366
  }
1263
1367
  return {
@@ -1266,7 +1370,12 @@ export function createSageStatusTool(mcpBridge) {
1266
1370
  type: "text",
1267
1371
  text: JSON.stringify({
1268
1372
  bridgeConnected,
1373
+ degraded: health.degraded,
1269
1374
  sageAdapterVersion: "0.2.0",
1375
+ lastHealthError: health.lastHealthError,
1376
+ lastHealthCheckedAt: health.lastHealthCheckedAt,
1377
+ availableToolCount: health.availableToolCount,
1378
+ availableToolNames: health.availableToolNames,
1270
1379
  }, null, 2),
1271
1380
  },
1272
1381
  {
@@ -1274,7 +1383,11 @@ export function createSageStatusTool(mcpBridge) {
1274
1383
  text: SAGE_CLI_REFERENCE,
1275
1384
  },
1276
1385
  ],
1277
- details: { bridgeConnected },
1386
+ details: {
1387
+ bridgeConnected,
1388
+ degraded: health.degraded,
1389
+ lastHealthError: health.lastHealthError,
1390
+ },
1278
1391
  };
1279
1392
  },
1280
1393
  };
@@ -1419,6 +1532,7 @@ export async function createSageSessionConfig(config = {}) {
1419
1532
  wrapToolsWithSecurity: wrapTools,
1420
1533
  setupHooks: (session) => {
1421
1534
  activeSessionId = session.sessionId;
1535
+ // Inject Sage session context (skill context from CLI).
1422
1536
  hookRuntime
1423
1537
  .getSessionContext(activeSessionId)
1424
1538
  .then(async (context) => {
@@ -1429,16 +1543,16 @@ export async function createSageSessionConfig(config = {}) {
1429
1543
  .catch((error) => {
1430
1544
  console.error("[sage-pi-adapter] failed to inject session context:", error);
1431
1545
  });
1432
- // Inject dynamic identity context (wallet, libraries, skills) at session start.
1546
+ // Show loaded skills/libraries summary to the user.
1433
1547
  hookRuntime
1434
- .getIdentityContext()
1435
- .then(async (identity) => {
1436
- if (!identity)
1548
+ .getSkillVisibilitySummary()
1549
+ .then(async (summary) => {
1550
+ if (!summary)
1437
1551
  return;
1438
- await injectHookMessage(session, "sage-identity-context", identity, false);
1552
+ await injectHookMessage(session, "sage-loaded", summary, true);
1439
1553
  })
1440
1554
  .catch((error) => {
1441
- console.error("[sage-pi-adapter] failed to inject identity context:", error);
1555
+ console.error("[sage-pi-adapter] failed to get skill summary:", error);
1442
1556
  });
1443
1557
  const unsubscribe = session.subscribe((event) => {
1444
1558
  const parsed = parseMessageEndEvent(event);