@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.
- package/README.md +33 -290
- package/SKILL.md +20 -2
- package/dist/sage-pi-adapter.d.ts +24 -11
- package/dist/sage-pi-adapter.d.ts.map +1 -1
- package/dist/sage-pi-adapter.js +242 -128
- package/dist/sage-pi-adapter.js.map +1 -1
- package/extensions/index.ts +303 -44
- package/package.json +2 -2
- package/skills/sage-bridge/SKILL.md +43 -5
package/extensions/index.ts
CHANGED
|
@@ -13,9 +13,92 @@ import {
|
|
|
13
13
|
type ExtensionApi = {
|
|
14
14
|
on: (eventName: string, handler: (event: any, ctx: any) => Promise<any> | any) => void;
|
|
15
15
|
registerTool: (tool: any) => void;
|
|
16
|
+
registerCommand: (name: string, options: { description: string; handler: (args: string, ctx: any) => Promise<void> | void }) => void;
|
|
16
17
|
sendMessage: (message: any, options?: any) => void;
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// ── Sage widget state machine ──
|
|
21
|
+
// Three independent layers that merge into widget lines without clobbering.
|
|
22
|
+
// Baseline persists across turns; suggestions and subagents are transient
|
|
23
|
+
// (cleared at start of each before_agent_start).
|
|
24
|
+
|
|
25
|
+
interface WidgetLayer {
|
|
26
|
+
lines: string[];
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class SageWidgetState {
|
|
31
|
+
baseline: WidgetLayer | null = null;
|
|
32
|
+
suggestions: WidgetLayer | null = null;
|
|
33
|
+
subagents: WidgetLayer | null = null;
|
|
34
|
+
|
|
35
|
+
/** Clear transient layers at the start of each turn. */
|
|
36
|
+
clearTransients(): void {
|
|
37
|
+
this.suggestions = null;
|
|
38
|
+
this.subagents = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Replace the baseline (static counts, active libs, exposed skills). */
|
|
42
|
+
setBaseline(text: string): void {
|
|
43
|
+
this.baseline = { lines: text.split('\n').filter(Boolean), updatedAt: Date.now() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Add suggestion count/skills to the transient layer. */
|
|
47
|
+
setSuggestions(keys: string[]): void {
|
|
48
|
+
if (keys.length === 0) {
|
|
49
|
+
this.suggestions = null;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const names = keys.map(k => k.startsWith('##') ? k.replace(/^##\s*/, '') : k);
|
|
53
|
+
const line = names.length === 1
|
|
54
|
+
? `↳ suggested: ${names[0]}`
|
|
55
|
+
: `↳ ${names.length} suggested: ${names.slice(0, 6).join(', ')}${names.length > 6 ? ` +${names.length - 6} more` : ''}`;
|
|
56
|
+
this.suggestions = { lines: [line], updatedAt: Date.now() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Add resolved subagent skill count to the transient layer. */
|
|
60
|
+
setSubagents(keys: string[]): void {
|
|
61
|
+
if (keys.length === 0) {
|
|
62
|
+
this.subagents = null;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.subagents = {
|
|
66
|
+
lines: [`↳ ${keys.length} subagent: ${keys.slice(0, 4).join(', ')}${keys.length > 4 ? ` +${keys.length - 4} more` : ''}`],
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Merge all layers into widget lines. */
|
|
72
|
+
render(): string[] {
|
|
73
|
+
const output: string[] = [];
|
|
74
|
+
if (this.baseline) output.push(...this.baseline.lines);
|
|
75
|
+
if (this.suggestions) output.push(...this.suggestions.lines);
|
|
76
|
+
if (this.subagents) output.push(...this.subagents.lines);
|
|
77
|
+
return output;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Push rendered output to the Pi widget. */
|
|
81
|
+
updateWidget(ui: any): void {
|
|
82
|
+
if (!ui?.setWidget) return;
|
|
83
|
+
const lines = this.render();
|
|
84
|
+
if (lines.length === 0) return;
|
|
85
|
+
ui.setWidget('sage', lines);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Extract skill keys from Sage suggestion output. */
|
|
90
|
+
function parseSuggestionKeys(text: string): string[] {
|
|
91
|
+
const keys: string[] = [];
|
|
92
|
+
for (const line of text.split('\n')) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed.startsWith('-')) continue;
|
|
95
|
+
// Match patterns like: "- code-audit" or "- code-audit (code-audit)" or "- code-audit - desc"
|
|
96
|
+
const m = trimmed.match(/^-\s+(\S+?)(?:\s+\(|$|\s+-)/);
|
|
97
|
+
if (m) keys.push(m[1]);
|
|
98
|
+
}
|
|
99
|
+
return keys;
|
|
100
|
+
}
|
|
101
|
+
|
|
19
102
|
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
|
|
20
103
|
if (value === undefined) return fallback;
|
|
21
104
|
const normalized = value.trim().toLowerCase();
|
|
@@ -226,6 +309,61 @@ function extractUsage(message: any): { input?: string; output?: string } {
|
|
|
226
309
|
};
|
|
227
310
|
}
|
|
228
311
|
|
|
312
|
+
function normalizeSkillPath(value: unknown): string | null {
|
|
313
|
+
if (typeof value !== 'string') return null;
|
|
314
|
+
const trimmed = value.trim();
|
|
315
|
+
if (!trimmed) return null;
|
|
316
|
+
return trimmed.replace(/^file:\/\//, '').replace(/\\+/g, '/').replace(/\/+/g, '/').toLowerCase();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function skillNameFromPath(path: string): string | null {
|
|
320
|
+
const normalized = normalizeSkillPath(path);
|
|
321
|
+
if (!normalized || !normalized.endsWith('/skill.md')) return null;
|
|
322
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
323
|
+
if (parts.length < 2) return null;
|
|
324
|
+
return parts[parts.length - 2] ?? null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function extractSkillCommandName(text: unknown): string | null {
|
|
328
|
+
if (typeof text !== 'string') return null;
|
|
329
|
+
const match = text.trim().match(/^\/skill:([a-z0-9][a-z0-9-]*)\b/i);
|
|
330
|
+
return match?.[1]?.toLowerCase() ?? null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function extractReadPath(input: unknown): string | null {
|
|
334
|
+
if (!input || typeof input !== 'object') return null;
|
|
335
|
+
const obj = input as Record<string, unknown>;
|
|
336
|
+
const raw = obj.file_path ?? obj.path;
|
|
337
|
+
return typeof raw === 'string' && raw.trim() ? raw.trim() : null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function refreshSkillPathIndex(skills: unknown, skillPathToName: Map<string, string>): void {
|
|
341
|
+
if (!Array.isArray(skills)) return;
|
|
342
|
+
|
|
343
|
+
for (const item of skills) {
|
|
344
|
+
if (!item || typeof item !== 'object') continue;
|
|
345
|
+
const skill = item as Record<string, unknown>;
|
|
346
|
+
const name = typeof skill.name === 'string' && skill.name.trim()
|
|
347
|
+
? skill.name.trim().toLowerCase()
|
|
348
|
+
: null;
|
|
349
|
+
if (!name) continue;
|
|
350
|
+
|
|
351
|
+
for (const key of ['filePath', 'file_path', 'path', 'location']) {
|
|
352
|
+
const normalized = normalizeSkillPath(skill[key]);
|
|
353
|
+
if (normalized) skillPathToName.set(normalized, name);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const baseDir = normalizeSkillPath(skill.baseDir ?? skill.dir ?? skill.directory);
|
|
357
|
+
if (baseDir) skillPathToName.set(`${baseDir}/skill.md`, name);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function skillNameFromReadEvent(event: any, skillPathToName: Map<string, string>): string | null {
|
|
362
|
+
const path = extractReadPath(event?.input ?? event?.args);
|
|
363
|
+
const normalized = normalizeSkillPath(path);
|
|
364
|
+
if (!normalized || !normalized.endsWith('/skill.md')) return null;
|
|
365
|
+
return skillPathToName.get(normalized) ?? skillNameFromPath(normalized);
|
|
366
|
+
}
|
|
229
367
|
|
|
230
368
|
/**
|
|
231
369
|
* Behavior-aware suggestion enrichment (Option A).
|
|
@@ -406,6 +544,7 @@ function formatBehaviorSuggestion(behavior: BehaviorMatch, prompt: string): stri
|
|
|
406
544
|
export default function sagePiAdapterExtension(pi: ExtensionApi): void {
|
|
407
545
|
const showHookMessages = parseBoolean(process.env.SAGE_SHOW_HOOK_MESSAGES, false);
|
|
408
546
|
const allowBashSageCli = parseBoolean(process.env.SAGE_ALLOW_BASH_CLI, true);
|
|
547
|
+
const enableBehaviorEnrich = parseBoolean(process.env.SAGE_BEHAVIOR_ENRICH, false);
|
|
409
548
|
const config: SageP2Config = {
|
|
410
549
|
sageBin: process.env.SAGE_BIN ?? 'sage',
|
|
411
550
|
sageProfile: process.env.SAGE_PROFILE,
|
|
@@ -454,12 +593,12 @@ export default function sagePiAdapterExtension(pi: ExtensionApi): void {
|
|
|
454
593
|
label: 'Sage: Behavior',
|
|
455
594
|
promptSnippet: 'Resolve and execute a Sage behavior plan (multi-skill workflow chain)',
|
|
456
595
|
description:
|
|
457
|
-
'Resolve a behavior
|
|
458
|
-
'
|
|
459
|
-
'entry-plan-review chains: sage → reverse-interviewing → pre-mortem →
|
|
460
|
-
'plan-eng-review → openprose → confidence-calibration. ' +
|
|
596
|
+
'Resolve and execute a Sage behavior: a multi-skill workflow whose resolved steps are ' +
|
|
597
|
+
'coordinated by the Pi behavior executor. Behaviors chain multiple skills into a linear ' +
|
|
598
|
+
'workflow — e.g., entry-plan-review chains: sage → reverse-interviewing → pre-mortem → ' +
|
|
599
|
+
'ceo-plan-review → plan-eng-review → openprose → confidence-calibration. ' +
|
|
461
600
|
'Call with key=\'entry-plan-review\' and task=\'Review my implementation plan\'. ' +
|
|
462
|
-
'The tool returns
|
|
601
|
+
'The tool returns the executor payload/result for the resolved skill chain.',
|
|
463
602
|
parameters: {
|
|
464
603
|
type: 'object' as const,
|
|
465
604
|
properties: {
|
|
@@ -525,6 +664,38 @@ export default function sagePiAdapterExtension(pi: ExtensionApi): void {
|
|
|
525
664
|
},
|
|
526
665
|
});
|
|
527
666
|
|
|
667
|
+
// Command: manually inspect Sage context injection
|
|
668
|
+
pi.registerCommand('sage-context', {
|
|
669
|
+
description: 'Show the Sage session context and loaded skills',
|
|
670
|
+
handler: async (_args: string, ctx: any) => {
|
|
671
|
+
const sessionId = getSessionId(ctx);
|
|
672
|
+
let output = '';
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
const context = await hookRuntime.getSessionContext(sessionId);
|
|
676
|
+
if (context) {
|
|
677
|
+
output += '## Injected Sage Context (hidden from UI)\n```\n' +
|
|
678
|
+
(context.length > 2000 ? context.slice(0, 2000) + '\n... (truncated)' : context) +
|
|
679
|
+
'\n```\n\n';
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
output += '(sage skill context --format pi failed)\n\n';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
const summary = await hookRuntime.getSkillVisibilitySummary();
|
|
687
|
+
if (summary) {
|
|
688
|
+
output += '## Visible Summary\n' + summary + '\n\n';
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
output += '(library summary failed)\n\n';
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
output += 'Tip: set SAGE_SHOW_HOOK_MESSAGES=1 to see context in the UI.';
|
|
695
|
+
ctx.ui.notify(output, 'info');
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
|
|
528
699
|
// ---------------------------------------------------------------------------
|
|
529
700
|
// System prompt injection: Sage-first routing for Pi sessions
|
|
530
701
|
// ---------------------------------------------------------------------------
|
|
@@ -534,8 +705,8 @@ export default function sagePiAdapterExtension(pi: ExtensionApi): void {
|
|
|
534
705
|
// Tier 2: Full usage patterns injected once at session_start (below).
|
|
535
706
|
// Tier 3: Complete CLI reference available on-demand via sage_status({}).
|
|
536
707
|
//
|
|
537
|
-
// This per-turn string is
|
|
538
|
-
//
|
|
708
|
+
// This per-turn string is intentionally small. Do not prepend the full
|
|
709
|
+
// `sage skill context` output here; that belongs in Tier 2 only.
|
|
539
710
|
const SAGE_PER_TURN_GUARDRAIL = `
|
|
540
711
|
[Sage Tools - Internet Access]
|
|
541
712
|
You have sage_search, sage_execute, and sage_status tools for internet and external service access.
|
|
@@ -544,9 +715,9 @@ Sage internal tools are available immediately. Only external MCP servers need li
|
|
|
544
715
|
`.trim();
|
|
545
716
|
|
|
546
717
|
// Tier 2: Full Sage protocol context injected ONCE at session_start.
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
718
|
+
// Prefer the dynamic CLI context because it is generated from the canonical
|
|
719
|
+
// Sage runtime templates. Keep the static block as a fallback only; appending
|
|
720
|
+
// both creates duplicate routing instructions in every Pi session.
|
|
550
721
|
const SAGE_SESSION_CONTEXT = `
|
|
551
722
|
## Sage MCP + CLI Routing
|
|
552
723
|
|
|
@@ -587,30 +758,77 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
587
758
|
|
|
588
759
|
let lastPrompt = '';
|
|
589
760
|
let lastSuggestionAt = 0;
|
|
761
|
+
const skillPathToName = new Map<string, string>();
|
|
762
|
+
const pendingInputSkillUses = new Set<string>();
|
|
763
|
+
let usedSkillsThisTurn = new Set<string>();
|
|
764
|
+
let pendingReadToolSkills = new Map<string, string>();
|
|
765
|
+
let currentPromptPreview = '';
|
|
766
|
+
let toolErrorSeenThisTurn = false;
|
|
767
|
+
const autoRecordSkillOutcome = parseBoolean(process.env.SAGE_AUTO_SKILL_OUTCOME, true);
|
|
768
|
+
|
|
769
|
+
// Sage widget state — baseline + per-turn transients
|
|
770
|
+
const sageWidget = new SageWidgetState();
|
|
771
|
+
|
|
772
|
+
const rememberSkillUse = (skillName: string | null | undefined): void => {
|
|
773
|
+
const normalized = typeof skillName === 'string' ? skillName.trim().toLowerCase() : '';
|
|
774
|
+
if (normalized) usedSkillsThisTurn.add(normalized);
|
|
775
|
+
};
|
|
776
|
+
|
|
590
777
|
pi.on('session_start', async (_event, ctx) => {
|
|
591
778
|
const sessionId = getSessionId(ctx);
|
|
592
779
|
|
|
593
|
-
//
|
|
594
|
-
// This includes skill trigger workflow, external tool patterns, recipes,
|
|
595
|
-
// and best practices — everything the model needs but only sees once.
|
|
780
|
+
// Inject full Sage protocol context once at session start.
|
|
596
781
|
let fullContext = SAGE_SESSION_CONTEXT;
|
|
597
782
|
try {
|
|
598
783
|
const dynamicContext = await hookRuntime.getSessionContext(sessionId);
|
|
599
784
|
if (dynamicContext.trim()) {
|
|
600
|
-
fullContext =
|
|
785
|
+
fullContext = dynamicContext.trim();
|
|
601
786
|
}
|
|
602
787
|
} catch {
|
|
603
|
-
// Keep static SAGE_SESSION_CONTEXT as fallback
|
|
788
|
+
// Keep static SAGE_SESSION_CONTEXT as fallback.
|
|
604
789
|
}
|
|
605
790
|
|
|
606
791
|
pi.sendMessage(
|
|
607
792
|
{
|
|
608
793
|
customType: 'sage-session-context',
|
|
609
794
|
content: fullContext,
|
|
610
|
-
display:
|
|
795
|
+
display: false,
|
|
611
796
|
},
|
|
612
797
|
{ deliverAs: 'nextTurn' }
|
|
613
798
|
);
|
|
799
|
+
|
|
800
|
+
// Update persistent skill widget via state machine.
|
|
801
|
+
// Fetch baseline once, render, and update widget.
|
|
802
|
+
try {
|
|
803
|
+
const summary = await hookRuntime.getSkillVisibilitySummary();
|
|
804
|
+
if (summary) {
|
|
805
|
+
sageWidget.setBaseline(summary);
|
|
806
|
+
sageWidget.updateWidget(ctx.ui);
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
// Non-blocking.
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Fallback: hook message for non-TUI modes
|
|
813
|
+
if (!ctx.ui?.setWidget) {
|
|
814
|
+
try {
|
|
815
|
+
const summary = await hookRuntime.getSkillVisibilitySummary();
|
|
816
|
+
if (summary) {
|
|
817
|
+
pi.sendMessage(
|
|
818
|
+
{ customType: 'sage-loaded', content: summary, display: true },
|
|
819
|
+
{ deliverAs: 'nextTurn' }
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
} catch {
|
|
823
|
+
// Non-blocking.
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
pi.on('input', async (event) => {
|
|
829
|
+
const skillName = extractSkillCommandName(event?.text);
|
|
830
|
+
if (skillName) pendingInputSkillUses.add(skillName);
|
|
831
|
+
return { action: 'continue' };
|
|
614
832
|
});
|
|
615
833
|
|
|
616
834
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
@@ -619,36 +837,28 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
619
837
|
const continuationPrompt = isContinuationPrompt(prompt);
|
|
620
838
|
|
|
621
839
|
const sessionId = getSessionId(ctx);
|
|
840
|
+
refreshSkillPathIndex(event?.systemPromptOptions?.skills, skillPathToName);
|
|
841
|
+
usedSkillsThisTurn = new Set(pendingInputSkillUses);
|
|
842
|
+
pendingInputSkillUses.clear();
|
|
843
|
+
pendingReadToolSkills = new Map<string, string>();
|
|
844
|
+
currentPromptPreview = prompt.slice(0, 240);
|
|
845
|
+
toolErrorSeenThisTurn = false;
|
|
846
|
+
|
|
847
|
+
// Clear per-turn transient widget layers
|
|
848
|
+
sageWidget.clearTransients();
|
|
849
|
+
|
|
622
850
|
await hookRuntime.capturePrompt(prompt, sessionId);
|
|
623
851
|
|
|
624
852
|
// --- System prompt injection (EVERY turn) ---
|
|
625
853
|
// Pi resets to _baseSystemPrompt if result.systemPrompt is undefined,
|
|
626
|
-
// so we must always return the modified prompt to keep
|
|
854
|
+
// so we must always return the modified prompt to keep the compact Sage
|
|
855
|
+
// guardrail active. Keep this block small: the full Sage context is sent
|
|
856
|
+
// once during session_start.
|
|
627
857
|
let modifiedSystemPrompt: string | undefined;
|
|
628
858
|
if (injectToolDocs) {
|
|
629
859
|
const currentSystemPrompt = typeof event?.systemPrompt === 'string' ? event.systemPrompt : '';
|
|
630
860
|
if (currentSystemPrompt) {
|
|
631
861
|
let docs = SAGE_PER_TURN_GUARDRAIL;
|
|
632
|
-
try {
|
|
633
|
-
const sessionContext = await hookRuntime.getSessionContext(sessionId);
|
|
634
|
-
if (sessionContext.trim()) {
|
|
635
|
-
docs = `${sessionContext.trim()}\n\n${SAGE_PER_TURN_GUARDRAIL}`;
|
|
636
|
-
}
|
|
637
|
-
} catch {
|
|
638
|
-
// Session-context injection is best-effort — never block the turn.
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Dynamic identity context: wallet, libraries, skill counts.
|
|
642
|
-
// Cached for 60s so the CLI cost is negligible per-turn.
|
|
643
|
-
try {
|
|
644
|
-
const identity = await hookRuntime.getIdentityContext();
|
|
645
|
-
if (identity) {
|
|
646
|
-
docs = `${identity}\n\n${docs}`;
|
|
647
|
-
}
|
|
648
|
-
} catch {
|
|
649
|
-
// Identity injection is best-effort — never block the turn.
|
|
650
|
-
}
|
|
651
|
-
|
|
652
862
|
modifiedSystemPrompt = currentSystemPrompt + '\n\n' + docs;
|
|
653
863
|
}
|
|
654
864
|
}
|
|
@@ -706,6 +916,9 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
706
916
|
skillBlocks.push(`\n--- Sage Skill: ${name} ---\n${content}\n--- End Skill: ${name} ---`);
|
|
707
917
|
}
|
|
708
918
|
modifiedSystemPrompt += '\n\n[Auto-resolved Sage Skills]\n' + skillBlocks.join('\n');
|
|
919
|
+
|
|
920
|
+
// Track for widget — rendered at end of before_agent_start
|
|
921
|
+
sageWidget.setSubagents([...resolved.keys()]);
|
|
709
922
|
}
|
|
710
923
|
} catch {
|
|
711
924
|
// Non-blocking — subagent proceeds without injected skills
|
|
@@ -726,11 +939,8 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
726
939
|
|
|
727
940
|
let suggestions = await hookRuntime.suggestSkill(prompt, sessionId);
|
|
728
941
|
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
// (full chain via sage_behavior OR individual skill via use_skill)
|
|
732
|
-
// and lists all steps for false-positive assessment.
|
|
733
|
-
if (suggestions.trim()) {
|
|
942
|
+
// Behavior enrichment — opt-in via SAGE_BEHAVIOR_ENRICH=1
|
|
943
|
+
if (suggestions.trim() && enableBehaviorEnrich) {
|
|
734
944
|
try {
|
|
735
945
|
suggestions = await enrichSuggestionsWithBehaviors(suggestions, prompt, bridge);
|
|
736
946
|
} catch {
|
|
@@ -744,9 +954,17 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
744
954
|
content: suggestions,
|
|
745
955
|
display: showHookMessages,
|
|
746
956
|
};
|
|
957
|
+
|
|
958
|
+
// Update widget with suggestion skill names
|
|
959
|
+
const suggestedKeys = parseSuggestionKeys(suggestions);
|
|
960
|
+
sageWidget.setSuggestions(suggestedKeys);
|
|
747
961
|
}
|
|
748
962
|
}
|
|
749
963
|
|
|
964
|
+
// Always push current widget state after processing suggestions.
|
|
965
|
+
// Transients may have been cleared or set; render them now.
|
|
966
|
+
sageWidget.updateWidget(ctx.ui);
|
|
967
|
+
|
|
750
968
|
// Return combined result if either system prompt or message changed
|
|
751
969
|
if (modifiedSystemPrompt || suggestionMessage) {
|
|
752
970
|
const result: Record<string, unknown> = {};
|
|
@@ -756,6 +974,28 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
756
974
|
}
|
|
757
975
|
});
|
|
758
976
|
|
|
977
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
978
|
+
const usedSkills = [...usedSkillsThisTurn];
|
|
979
|
+
if (usedSkills.length === 0) return;
|
|
980
|
+
|
|
981
|
+
const sessionId = getSessionId(ctx);
|
|
982
|
+
await hookRuntime.recordSuggestionUse(usedSkills, sessionId);
|
|
983
|
+
|
|
984
|
+
if (autoRecordSkillOutcome) {
|
|
985
|
+
const status = toolErrorSeenThisTurn ? 'failed' : 'passed';
|
|
986
|
+
await Promise.allSettled(
|
|
987
|
+
usedSkills.map((skillName) =>
|
|
988
|
+
hookRuntime.recordExecutionOutcome(skillName, status, currentPromptPreview, sessionId)
|
|
989
|
+
)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
usedSkillsThisTurn = new Set<string>();
|
|
994
|
+
pendingReadToolSkills = new Map<string, string>();
|
|
995
|
+
currentPromptPreview = '';
|
|
996
|
+
toolErrorSeenThisTurn = false;
|
|
997
|
+
});
|
|
998
|
+
|
|
759
999
|
pi.on('message_end', async (event, ctx) => {
|
|
760
1000
|
const message = event?.message;
|
|
761
1001
|
if (!message || typeof message !== 'object') return;
|
|
@@ -773,7 +1013,15 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
773
1013
|
|
|
774
1014
|
pi.on('tool_call', async (event, ctx) => {
|
|
775
1015
|
const toolName = typeof event?.toolName === 'string' ? event.toolName : '';
|
|
776
|
-
if (!toolName
|
|
1016
|
+
if (!toolName) return;
|
|
1017
|
+
|
|
1018
|
+
if (toolName === 'read') {
|
|
1019
|
+
const skillName = skillNameFromReadEvent(event, skillPathToName);
|
|
1020
|
+
const toolCallId = typeof event?.toolCallId === 'string' ? event.toolCallId : '';
|
|
1021
|
+
if (skillName && toolCallId) pendingReadToolSkills.set(toolCallId, skillName);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (toolName.startsWith('sage_')) return;
|
|
777
1025
|
|
|
778
1026
|
const toolInput =
|
|
779
1027
|
event?.input && typeof event.input === 'object'
|
|
@@ -855,7 +1103,18 @@ Use these tools proactively when the user asks about current events, needs docum
|
|
|
855
1103
|
|
|
856
1104
|
pi.on('tool_result', async (event, ctx) => {
|
|
857
1105
|
const toolName = typeof event?.toolName === 'string' ? event.toolName : '';
|
|
858
|
-
if (!toolName
|
|
1106
|
+
if (!toolName) return;
|
|
1107
|
+
|
|
1108
|
+
if (Boolean(event?.isError)) toolErrorSeenThisTurn = true;
|
|
1109
|
+
|
|
1110
|
+
if (toolName === 'read' && !Boolean(event?.isError)) {
|
|
1111
|
+
const toolCallId = typeof event?.toolCallId === 'string' ? event.toolCallId : '';
|
|
1112
|
+
const skillName = skillNameFromReadEvent(event, skillPathToName) ?? pendingReadToolSkills.get(toolCallId);
|
|
1113
|
+
rememberSkillUse(skillName);
|
|
1114
|
+
if (toolCallId) pendingReadToolSkills.delete(toolCallId);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (toolName.startsWith('sage_')) return;
|
|
859
1118
|
|
|
860
1119
|
const toolInput =
|
|
861
1120
|
event?.input && typeof event.input === 'object'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sage-protocol/pi-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Sage Protocol MCP integration for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@sinclair/typebox": "^0.34.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=0.64.0 <0.80.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^24.0.0",
|
|
@@ -50,6 +50,11 @@ sage_status({})
|
|
|
50
50
|
|
|
51
51
|
Returns bridge health, wallet connection, network, and session info.
|
|
52
52
|
|
|
53
|
+
If `sage_status` reports a degraded or unavailable bridge, do not keep retrying
|
|
54
|
+
silently. Tell the user the Sage MCP bridge is unavailable and suggest
|
|
55
|
+
restarting Pi/Sage bridge or verifying `sage mcp start`. Only suggest
|
|
56
|
+
`sage daemon start` when the reported failure is specifically daemon reachability.
|
|
57
|
+
|
|
53
58
|
---
|
|
54
59
|
|
|
55
60
|
## Behavior Loop
|
|
@@ -85,13 +90,40 @@ sage_search({ domain: "rlm", action: "stats" })
|
|
|
85
90
|
**5. Earn & Collaborate** — Publish, vote, tip, bounty.
|
|
86
91
|
|
|
87
92
|
```bash
|
|
88
|
-
sage library push my-library
|
|
93
|
+
sage library push my-library --bump patch
|
|
89
94
|
sage governance proposals vote 1 for
|
|
90
95
|
sage bounties list
|
|
91
96
|
```
|
|
92
97
|
|
|
93
98
|
---
|
|
94
99
|
|
|
100
|
+
## Choose a Sage distribution surface by intent
|
|
101
|
+
|
|
102
|
+
Do not collapse every write path into a generic idea of "publish." Choose the smallest surface that matches the actual goal:
|
|
103
|
+
|
|
104
|
+
- **P2P** — sync trusted machines/agents privately.
|
|
105
|
+
- **Shared library** — collaborate privately with explicit members/invites.
|
|
106
|
+
- **Personal cloud** — host a creator-controlled canonical library; it starts private by default.
|
|
107
|
+
- **Public personal cloud** — make a hosted library publicly discoverable only when the user explicitly asks, with `sage library visibility <library-id> public`.
|
|
108
|
+
- **Marketplace** — sell a polished public personal library as a packaged capability.
|
|
109
|
+
- **DAO promotion** — make a library canonical for a group/community and unlock curation, legitimacy, on-chain reputation, tips, bounties, and community-driven improvement.
|
|
110
|
+
|
|
111
|
+
**Governance chooser**
|
|
112
|
+
- **personal DAO** — fastest operator-controlled canon
|
|
113
|
+
- **team DAO** — trusted shared stewardship
|
|
114
|
+
- **community DAO** — strongest public curation and legitimacy
|
|
115
|
+
|
|
116
|
+
Pi routing rules:
|
|
117
|
+
- If the user says **direct share** or **share privately**, think **shared library** first.
|
|
118
|
+
- If the goal is trusted fleet propagation, think **P2P**, not governance.
|
|
119
|
+
- If the goal is creator-controlled hosting, think **personal cloud** and remember it is private by default.
|
|
120
|
+
- If the goal is public web/discovery access, confirm explicit intent before making a library public.
|
|
121
|
+
- If the goal is monetization, think **marketplace**.
|
|
122
|
+
- If the goal is public canon, stronger provenance, future bounties, and repeated community improvement, think **DAO promotion**.
|
|
123
|
+
- If one surface is clearly implied, proactively suggest it instead of waiting for the user to name the exact Sage term.
|
|
124
|
+
- Install/expose is local harness visibility only; do not treat it as publishing.
|
|
125
|
+
- Public feeds/search should only advertise anonymously readable prompts and skills; keep owner-only, unpinned, expired, or otherwise non-openable CIDs out of public discovery until republished or made public.
|
|
126
|
+
|
|
95
127
|
## When to Trigger
|
|
96
128
|
|
|
97
129
|
Proactively search Sage when:
|
|
@@ -131,7 +163,7 @@ Recommended `.pi/settings.json` shape in a local monorepo:
|
|
|
131
163
|
"-../.agents/skills/frontend-design/SKILL.md",
|
|
132
164
|
"-~/.pi/agent/skills/prompt-builder/SKILL.md",
|
|
133
165
|
"-~/.pi/agent/skills/sage/SKILL.md",
|
|
134
|
-
"-~/.pi/agent/skills/sage-
|
|
166
|
+
"-~/.pi/agent/skills/sage-workflow/SKILL.md"
|
|
135
167
|
],
|
|
136
168
|
"enableSkillCommands": true
|
|
137
169
|
}
|
|
@@ -190,8 +222,9 @@ sage library list
|
|
|
190
222
|
sage library create <name>
|
|
191
223
|
sage library skill add <path> -l <library>
|
|
192
224
|
sage library prompt add <name> -l <library> --file <path>
|
|
193
|
-
sage library push <library>
|
|
225
|
+
sage library push <library> --bump patch
|
|
194
226
|
sage library promote <library> --dao 0x... --collection default --yes
|
|
227
|
+
# Use `major`, `minor`, or `patch` for `--bump`; exact SemVer and `status --across` are not current CLI commands.
|
|
195
228
|
sage library discover
|
|
196
229
|
sage library sync
|
|
197
230
|
```
|
|
@@ -237,7 +270,9 @@ sage social post "message"
|
|
|
237
270
|
```bash
|
|
238
271
|
sage wallet balance
|
|
239
272
|
sage wallet current
|
|
240
|
-
sage wallet
|
|
273
|
+
sage wallet create sage-wallet
|
|
274
|
+
sage wallet connect ows -n sage-wallet
|
|
275
|
+
sage wallet connect privy --device-code
|
|
241
276
|
sage wallet delegate
|
|
242
277
|
sage delegation create --delegation-profile voter
|
|
243
278
|
sage delegation list
|
|
@@ -247,7 +282,9 @@ sage agent register
|
|
|
247
282
|
sage agent quickstart
|
|
248
283
|
```
|
|
249
284
|
|
|
250
|
-
|
|
285
|
+
Preserve the user's already-working wallet/session preference when one exists. The examples above show common direct-wallet and provider-session paths, not a requirement to force one provider.
|
|
286
|
+
|
|
287
|
+
`sage wallet delegate` is ERC20Votes self-delegation for governance voting power. `sage delegation ...` is scoped agent delegation management.
|
|
251
288
|
|
|
252
289
|
### RLM Capture
|
|
253
290
|
|
|
@@ -274,6 +311,7 @@ sage mcp call <tool> --args '{...}'
|
|
|
274
311
|
## Common Pitfalls
|
|
275
312
|
|
|
276
313
|
- **Hook mode vs query mode**: `sage suggest hook skill` reads the `PROMPT` environment variable or stdin — don't pass the prompt as a positional argument.
|
|
314
|
+
- **Bridge vs daemon wording**: Pi primarily talks to Sage through the MCP bridge. When `sage_status` shows bridge degradation, say "Sage MCP bridge unavailable" and verify `sage mcp start`; don't default to daemon-start advice unless the daemon is the reported failure.
|
|
277
315
|
- **`external/search` requires `tool_name`**: Always include `tool_name` in params when searching via external domain (e.g., `tool_name: "web_search"`).
|
|
278
316
|
- **Domain routing**: Each domain has its own set of actions. Use `sage_search({ domain: "help", action: "list" })` to discover available actions.
|
|
279
317
|
- **Legacy commands removed**: Do not use `sage dao ...`, `sage proposals ...`, or `sage ipfs ...` — these are replaced by `sage governance dao ...`, `sage governance proposals ...`, and `sage config ipfs ...`.
|