@sage-protocol/pi-adapter 0.2.2 → 0.3.0

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,72 +881,26 @@ 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.
801
- *
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.
888
+ * Gather skill visibility summary for user-facing display.
805
889
  *
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 concise one-line summary of what libraries and skills are
892
+ * available. 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
+ let totalSkills = 0;
900
+ let totalPrompts = 0;
901
+ let libraryCount = 0;
857
902
  const libraryOut = libraryResult.status === "fulfilled" ? libraryResult.value?.stdout.trim() ?? "" : "";
858
903
  if (libraryOut) {
859
- let totalSkills = 0;
860
- let totalPrompts = 0;
861
- let libraryCount = 0;
862
904
  for (const line of libraryOut.split("\n")) {
863
905
  const m = line.match(/\((\d+)\s+prompts?,\s*(\d+)\s+skills?\)/);
864
906
  if (m) {
@@ -867,31 +909,29 @@ export class SageHookRuntime {
867
909
  totalSkills += Number.parseInt(m[2], 10);
868
910
  }
869
911
  }
870
- if (libraryCount > 0) {
871
- lines.push(`- ${libraryCount} libraries, ${totalSkills} skills, ${totalPrompts} prompts installed`);
912
+ }
913
+ const activeOut = activeResult.status === "fulfilled" ? activeResult.value?.stdout.trim() ?? "" : "";
914
+ let activeCount = 0;
915
+ if (activeOut) {
916
+ for (const line of activeOut.split("\n")) {
917
+ if (/^\s*\d+\.\s+/.test(line))
918
+ activeCount++;
872
919
  }
873
920
  }
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;
921
+ const parts = [];
922
+ if (libraryCount > 0) {
923
+ parts.push(`${libraryCount} libraries`);
924
+ }
925
+ if (totalSkills > 0) {
926
+ parts.push(`${totalSkills} skills`);
927
+ }
928
+ if (totalPrompts > 0) {
929
+ parts.push(`${totalPrompts} prompts`);
930
+ }
931
+ if (activeCount > 0) {
932
+ parts.push(`${activeCount} active`);
933
+ }
934
+ return parts.length > 0 ? `[Sage] ${parts.join(", ")} loaded` : "";
895
935
  }
896
936
  async suggestSkill(prompt, sessionId) {
897
937
  const trimmed = prompt.trim();
@@ -936,6 +976,32 @@ export class SageHookRuntime {
936
976
  TOKENS_OUTPUT: usage.output ?? "",
937
977
  }, JSON.stringify({ response: trimmed }), true);
938
978
  }
979
+ async recordSuggestionUse(skillKeys, sessionId) {
980
+ if (this.config.enableRlmFeedback === false)
981
+ return;
982
+ const used = [...new Set(skillKeys.map((key) => key.trim()).filter(Boolean))];
983
+ if (used.length === 0)
984
+ return;
985
+ await this.run(["suggest", "feedback", "use", ...used.flatMap((key) => ["--used", key])], {
986
+ SAGE_SOURCE: this.config.source ?? "pi",
987
+ SAGE_SESSION_ID: sessionId ?? "",
988
+ }, undefined, true);
989
+ }
990
+ async recordExecutionOutcome(skillKey, status, queryPreview, sessionId) {
991
+ if (this.config.enableRlmFeedback === false)
992
+ return;
993
+ const key = skillKey.trim();
994
+ if (!key)
995
+ return;
996
+ const args = ["suggest", "feedback", "outcome", key, "--status", status];
997
+ const preview = queryPreview?.trim();
998
+ if (preview)
999
+ args.push("--query-preview", preview.slice(0, 240));
1000
+ await this.run(args, {
1001
+ SAGE_SOURCE: this.config.source ?? "pi",
1002
+ SAGE_SESSION_ID: sessionId ?? "",
1003
+ }, undefined, true);
1004
+ }
939
1005
  async scanPreTool(toolName, toolInput, sessionId) {
940
1006
  if (this.config.enableSecurityHooks === false)
941
1007
  return null;
@@ -992,6 +1058,7 @@ export class SageHookRuntime {
992
1058
  const commandEnv = {
993
1059
  HOME: process.env.HOME ?? "",
994
1060
  PATH: process.env.PATH ?? "",
1061
+ ...sagePassthroughEnv(),
995
1062
  ...(this.config.env ?? {}),
996
1063
  ...env,
997
1064
  };
@@ -1188,39 +1255,21 @@ export function createSageExecuteTool(mcpBridge) {
1188
1255
  },
1189
1256
  };
1190
1257
  }
1191
- // Tier 3: Complete CLI reference — available on-demand via sage_status().
1192
- // Not injected into system prompt; model requests it when needed.
1193
1258
  const SAGE_CLI_REFERENCE = `
1194
- ## Sage CLI Reference (Tier 3 — on-demand)
1259
+ ## Sage CLI Reference (on-demand)
1195
1260
 
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)
1261
+ ### Skill Discovery
1262
+ - sage_search({ domain: "skills", action: "search", params: { query: "..." } })
1263
+ - sage_search({ domain: "skills", action: "get", params: { key: "..." } })
1201
1264
 
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
1265
+ ### External Tools
1266
+ - Web search: sage_search({ domain: "external", action: "search", params: { tool_name: "web_search", tool_params: { query: "..." } } })
1267
+ - URL fetch: sage_execute({ domain: "external", action: "call", params: { tool_name: "fetch", tool_params: { url: "https://..." } } })
1268
+ - List tools: sage_search({ domain: "external", action: "list_servers" })
1209
1269
 
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...
1214
-
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 ...
1270
+ ### Hub
1271
+ - Start server: sage_execute({ domain: "hub", action: "start", params: { server_id: "brave-search" } })
1272
+ - Stop server: sage_execute({ domain: "hub", action: "stop", params: { server_id: "brave-search" } })
1224
1273
  `.trim();
1225
1274
  export function createSageStatusTool(mcpBridge) {
1226
1275
  return {
@@ -1231,7 +1280,8 @@ export function createSageStatusTool(mcpBridge) {
1231
1280
  "Call this to see what external tools (web search, GitHub, fetch, browser automation, etc.) are available.",
1232
1281
  parameters: Type.Object({}),
1233
1282
  execute: async () => {
1234
- const bridgeConnected = mcpBridge.isReady();
1283
+ const health = mcpBridge.getHealthSnapshot();
1284
+ const bridgeConnected = health.bridgeConnected && !health.degraded;
1235
1285
  if (bridgeConnected) {
1236
1286
  try {
1237
1287
  const context = await mcpBridge.callCodeModeSearch({
@@ -1245,7 +1295,10 @@ export function createSageStatusTool(mcpBridge) {
1245
1295
  type: "text",
1246
1296
  text: JSON.stringify({
1247
1297
  bridgeConnected,
1298
+ degraded: false,
1248
1299
  sageAdapterVersion: "0.2.0",
1300
+ availableToolCount: health.availableToolCount,
1301
+ availableToolNames: health.availableToolNames,
1249
1302
  context: extractJsonFromMcpResult(context) ?? context,
1250
1303
  }, null, 2),
1251
1304
  },
@@ -1256,8 +1309,27 @@ export function createSageStatusTool(mcpBridge) {
1256
1309
  ],
1257
1310
  };
1258
1311
  }
1259
- catch {
1260
- // no-op, fallback below
1312
+ catch (error) {
1313
+ // Tool call failed even though the bridge claims ready; surface as
1314
+ // degraded rather than falling back silently.
1315
+ return {
1316
+ content: [
1317
+ {
1318
+ type: "text",
1319
+ text: JSON.stringify({
1320
+ bridgeConnected,
1321
+ degraded: true,
1322
+ sageAdapterVersion: "0.2.0",
1323
+ lastHealthError: error instanceof Error ? error.message : String(error),
1324
+ }, null, 2),
1325
+ },
1326
+ {
1327
+ type: "text",
1328
+ text: SAGE_CLI_REFERENCE,
1329
+ },
1330
+ ],
1331
+ details: { bridgeConnected, degraded: true },
1332
+ };
1261
1333
  }
1262
1334
  }
1263
1335
  return {
@@ -1266,7 +1338,12 @@ export function createSageStatusTool(mcpBridge) {
1266
1338
  type: "text",
1267
1339
  text: JSON.stringify({
1268
1340
  bridgeConnected,
1341
+ degraded: health.degraded,
1269
1342
  sageAdapterVersion: "0.2.0",
1343
+ lastHealthError: health.lastHealthError,
1344
+ lastHealthCheckedAt: health.lastHealthCheckedAt,
1345
+ availableToolCount: health.availableToolCount,
1346
+ availableToolNames: health.availableToolNames,
1270
1347
  }, null, 2),
1271
1348
  },
1272
1349
  {
@@ -1274,7 +1351,11 @@ export function createSageStatusTool(mcpBridge) {
1274
1351
  text: SAGE_CLI_REFERENCE,
1275
1352
  },
1276
1353
  ],
1277
- details: { bridgeConnected },
1354
+ details: {
1355
+ bridgeConnected,
1356
+ degraded: health.degraded,
1357
+ lastHealthError: health.lastHealthError,
1358
+ },
1278
1359
  };
1279
1360
  },
1280
1361
  };
@@ -1419,6 +1500,7 @@ export async function createSageSessionConfig(config = {}) {
1419
1500
  wrapToolsWithSecurity: wrapTools,
1420
1501
  setupHooks: (session) => {
1421
1502
  activeSessionId = session.sessionId;
1503
+ // Inject Sage session context (skill context from CLI).
1422
1504
  hookRuntime
1423
1505
  .getSessionContext(activeSessionId)
1424
1506
  .then(async (context) => {
@@ -1429,16 +1511,16 @@ export async function createSageSessionConfig(config = {}) {
1429
1511
  .catch((error) => {
1430
1512
  console.error("[sage-pi-adapter] failed to inject session context:", error);
1431
1513
  });
1432
- // Inject dynamic identity context (wallet, libraries, skills) at session start.
1514
+ // Show loaded skills/libraries summary to the user.
1433
1515
  hookRuntime
1434
- .getIdentityContext()
1435
- .then(async (identity) => {
1436
- if (!identity)
1516
+ .getSkillVisibilitySummary()
1517
+ .then(async (summary) => {
1518
+ if (!summary)
1437
1519
  return;
1438
- await injectHookMessage(session, "sage-identity-context", identity, false);
1520
+ await injectHookMessage(session, "sage-loaded", summary, true);
1439
1521
  })
1440
1522
  .catch((error) => {
1441
- console.error("[sage-pi-adapter] failed to inject identity context:", error);
1523
+ console.error("[sage-pi-adapter] failed to get skill summary:", error);
1442
1524
  });
1443
1525
  const unsubscribe = session.subscribe((event) => {
1444
1526
  const parsed = parseMessageEndEvent(event);