@hera-al/server 1.6.2 → 1.6.4
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/dist/agent/prompt-builder.js +1 -1
- package/dist/agent/session-agent.d.ts +26 -0
- package/dist/agent/session-agent.js +1 -1
- package/dist/commands/model.d.ts +11 -2
- package/dist/commands/model.js +1 -1
- package/dist/commands/models.d.ts +4 -2
- package/dist/commands/models.js +1 -1
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +1 -1
- package/dist/config.d.ts +61 -4
- package/dist/config.js +1 -1
- package/dist/gateway/channels/telegram.js +1 -1
- package/dist/nostromo/nostromo.js +1 -1
- package/dist/nostromo/ui-html-layout.js +1 -1
- package/dist/nostromo/ui-js-agent.js +1 -1
- package/dist/nostromo/ui-js-config.js +1 -1
- package/dist/nostromo/ui-js-core.js +1 -1
- package/dist/nostromo/ui-styles.js +1 -1
- package/dist/pi-agent-provider/index.d.ts +115 -0
- package/dist/pi-agent-provider/index.js +1 -0
- package/dist/pi-agent-provider/integration-example.d.ts +83 -0
- package/dist/pi-agent-provider/integration-example.js +1 -0
- package/dist/pi-agent-provider/pi-context-compactor.d.ts +116 -0
- package/dist/pi-agent-provider/pi-context-compactor.js +1 -0
- package/dist/pi-agent-provider/pi-mcp-bridge.d.ts +38 -0
- package/dist/pi-agent-provider/pi-mcp-bridge.js +1 -0
- package/dist/pi-agent-provider/pi-message-adapter.d.ts +93 -0
- package/dist/pi-agent-provider/pi-message-adapter.js +1 -0
- package/dist/pi-agent-provider/pi-query.d.ts +49 -0
- package/dist/pi-agent-provider/pi-query.js +1 -0
- package/dist/pi-agent-provider/pi-skill-loader.d.ts +15 -0
- package/dist/pi-agent-provider/pi-skill-loader.js +1 -0
- package/dist/pi-agent-provider/pi-tool-adapter.d.ts +105 -0
- package/dist/pi-agent-provider/pi-tool-adapter.js +1 -0
- package/dist/pi-agent-provider/pi-tool-executor.d.ts +95 -0
- package/dist/pi-agent-provider/pi-tool-executor.js +1 -0
- package/dist/pi-agent-provider/pi-types.d.ts +179 -0
- package/dist/pi-agent-provider/pi-types.js +1 -0
- package/dist/server.js +1 -1
- package/dist/stt/stt-loader.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Example: Using pi-agent-provider with Hera's SessionAgent
|
|
3
|
+
*
|
|
4
|
+
* This file shows exactly how to modify session-agent.ts to use piQuery()
|
|
5
|
+
* instead of the Claude Agent SDK's query().
|
|
6
|
+
*
|
|
7
|
+
* There are TWO integration strategies:
|
|
8
|
+
*
|
|
9
|
+
* 1. MINIMAL (recommended): Replace only the query() call in ensureInitialized()
|
|
10
|
+
* - SessionAgent.processOutput() stays exactly the same
|
|
11
|
+
* - All queue modes, debounce, cap, etc. work unchanged
|
|
12
|
+
* - piQuery() yields the same SDKMessage types
|
|
13
|
+
*
|
|
14
|
+
* 2. FULL: Use pi-agent-core's Agent class with its own event loop
|
|
15
|
+
* - More idiomatic pi-mono usage
|
|
16
|
+
* - Requires more changes to SessionAgent
|
|
17
|
+
* - Better streaming support
|
|
18
|
+
*/
|
|
19
|
+
import type { PiProviderConfig } from "./pi-types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Example configurations for different providers.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PROVIDER_CONFIGS: Record<string, PiProviderConfig>;
|
|
24
|
+
/**
|
|
25
|
+
* Here's the exact diff you'd apply to session-agent.ts:
|
|
26
|
+
*
|
|
27
|
+
* ```diff
|
|
28
|
+
* --- a/src/agent/session-agent.ts
|
|
29
|
+
* +++ b/src/agent/session-agent.ts
|
|
30
|
+
* @@ -36,1 +36,3 @@
|
|
31
|
+
* -import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
32
|
+
* +// import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
33
|
+
* +import { piQuery, createToolRegistryFromOptions } from "@hera-al/pi-agent-provider";
|
|
34
|
+
* +import type { PiProviderConfig, PiQueryHandle } from "@hera-al/pi-agent-provider";
|
|
35
|
+
*
|
|
36
|
+
* @@ -44,1 +46,1 @@
|
|
37
|
+
* -type QueryHandle = ReturnType<typeof query>;
|
|
38
|
+
* +type QueryHandle = PiQueryHandle;
|
|
39
|
+
*
|
|
40
|
+
* @@ -148,6 +150,8 @@
|
|
41
|
+
* export class SessionAgent {
|
|
42
|
+
* private queue: MessageQueue;
|
|
43
|
+
* private queryHandle: QueryHandle | null = null;
|
|
44
|
+
* + // Pi Agent provider configuration
|
|
45
|
+
* + private piProviderConfig: PiProviderConfig;
|
|
46
|
+
*
|
|
47
|
+
* @@ -215,6 +219,8 @@
|
|
48
|
+
* constructor(
|
|
49
|
+
* private sessionKey: string,
|
|
50
|
+
* private config: AppConfig,
|
|
51
|
+
* + // NEW: Pi Agent provider config
|
|
52
|
+
* + piProviderConfig: PiProviderConfig,
|
|
53
|
+
* systemPrompt: string,
|
|
54
|
+
* ...
|
|
55
|
+
* ) {
|
|
56
|
+
* + this.piProviderConfig = piProviderConfig;
|
|
57
|
+
*
|
|
58
|
+
* @@ -1143,7 +1149,10 @@
|
|
59
|
+
* private ensureInitialized(): void {
|
|
60
|
+
* if (this.initialized) return;
|
|
61
|
+
* this.initialized = true;
|
|
62
|
+
* - this.queryHandle = query({
|
|
63
|
+
* - prompt: this.queue as any,
|
|
64
|
+
* - options: this.opts,
|
|
65
|
+
* - });
|
|
66
|
+
* + // Create tool registry and start piQuery
|
|
67
|
+
* + createToolRegistryFromOptions(this.opts).then((toolRegistry) => {
|
|
68
|
+
* + this.queryHandle = piQuery(
|
|
69
|
+
* + { prompt: this.queue, options: this.opts },
|
|
70
|
+
* + this.piProviderConfig,
|
|
71
|
+
* + toolRegistry,
|
|
72
|
+
* + );
|
|
73
|
+
* + this.processOutput();
|
|
74
|
+
* + });
|
|
75
|
+
* - this.processOutput();
|
|
76
|
+
* + // Note: processOutput is called after async tool registry setup
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* That's it. ~20 lines changed. processOutput() is 100% unchanged because
|
|
81
|
+
* piQuery() yields the exact same message types.
|
|
82
|
+
*/
|
|
83
|
+
//# sourceMappingURL=integration-example.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{piQuery as e}from"./pi-query.js";import{createToolRegistryFromOptions as r}from"./pi-mcp-bridge.js";export const PROVIDER_CONFIGS={"openai-gpt4o":{provider:"openai",modelId:"gpt-4o",temperature:.7},"openai-gpt4o-mini":{provider:"openai",modelId:"gpt-4o-mini",temperature:.7},"anthropic-sonnet":{provider:"anthropic",modelId:"claude-sonnet-4-20250514",temperature:.7},"google-gemini":{provider:"google",modelId:"gemini-2.5-pro",temperature:.7},"openrouter-llama":{provider:"openrouter",modelId:"meta-llama/llama-3.1-405b-instruct",temperature:.7},"xai-grok":{provider:"xai",modelId:"grok-3",temperature:.7},"groq-llama":{provider:"groq",modelId:"llama-3.1-70b-versatile",temperature:.7}};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Agent Context Compaction System
|
|
3
|
+
*
|
|
4
|
+
* Three-phase cascading compaction to manage the context window:
|
|
5
|
+
*
|
|
6
|
+
* Phase 1 — Observation Masking (deterministic, no LLM call)
|
|
7
|
+
* Truncates large tool results keeping a header + first N chars.
|
|
8
|
+
*
|
|
9
|
+
* Phase 2 — Reference Compaction (deterministic, no LLM call)
|
|
10
|
+
* Replaces tool results with minimal one-line references.
|
|
11
|
+
*
|
|
12
|
+
* Phase 3 — Structured Summarization (requires LLM call)
|
|
13
|
+
* Replaces old messages with a structured summary.
|
|
14
|
+
*
|
|
15
|
+
* Key design: compaction happens at the START of each processUserMessage(),
|
|
16
|
+
* BEFORE the new user message is pushed. At that point, every message in the
|
|
17
|
+
* array is from previous user-request cycles — the model has already read and
|
|
18
|
+
* acted on those tool results. Within a single cycle (U→tools→R), all tool
|
|
19
|
+
* outputs remain full so the model can reason on them without loss.
|
|
20
|
+
*/
|
|
21
|
+
import type { PiMessage } from "./pi-message-adapter.js";
|
|
22
|
+
export interface CompactionConfig {
|
|
23
|
+
/** Estimated max tokens for the model's context window (default: 128000) */
|
|
24
|
+
contextWindowTokens: number;
|
|
25
|
+
/** Phase 1: char threshold per tool result to trigger masking (default: 3000) */
|
|
26
|
+
maskCharThreshold: number;
|
|
27
|
+
/** Phase 1: chars to preserve at the head of masked content (default: 500) */
|
|
28
|
+
maskHeadChars: number;
|
|
29
|
+
/** Phase 2: token ratio threshold to trigger reference compaction (default: 0.60) */
|
|
30
|
+
referenceThresholdRatio: number;
|
|
31
|
+
/** Phase 3: token ratio threshold to trigger summarization (default: 0.90) */
|
|
32
|
+
summarizationThresholdRatio: number;
|
|
33
|
+
/** Number of recent messages to keep raw during Phase 3 summarization (default: 6) */
|
|
34
|
+
keepLastNMessages: number;
|
|
35
|
+
/** Tool names whose results should never be masked or compacted (substring match) */
|
|
36
|
+
preserveToolNames: string[];
|
|
37
|
+
/** Optional alternative model for Phase 3 summarization (format: "provider/modelId") */
|
|
38
|
+
summarizationModel?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface CompactionResult {
|
|
41
|
+
phase: 0 | 1 | 2 | 3;
|
|
42
|
+
estimatedTokensBefore: number;
|
|
43
|
+
estimatedTokensAfter: number;
|
|
44
|
+
maskedResults: number;
|
|
45
|
+
compactedResults: number;
|
|
46
|
+
summarized: boolean;
|
|
47
|
+
/** If Phase 3 activated, contains the prompt for the summarization LLM call */
|
|
48
|
+
summarizationPrompt?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_COMPACTION_CONFIG: CompactionConfig;
|
|
51
|
+
/** Estimate total tokens for the session (chars/4 heuristic) */
|
|
52
|
+
export declare function estimateSessionTokens(messages: PiMessage[], systemPrompt: string): number;
|
|
53
|
+
/**
|
|
54
|
+
* Phase 1: Mask old tool results that exceed the char threshold.
|
|
55
|
+
* Preserves a header + first N chars of content.
|
|
56
|
+
* Modifies messages in-place.
|
|
57
|
+
*
|
|
58
|
+
* @param startIndex - Start scanning from this index (skip already-compacted segments)
|
|
59
|
+
* @param boundary - Stop scanning at this index (exclusive)
|
|
60
|
+
* @returns Number of results masked
|
|
61
|
+
*/
|
|
62
|
+
export declare function maskOldObservations(messages: PiMessage[], startIndex: number, boundary: number, opts: {
|
|
63
|
+
charThreshold: number;
|
|
64
|
+
headChars: number;
|
|
65
|
+
preserveToolNames: string[];
|
|
66
|
+
}): number;
|
|
67
|
+
/**
|
|
68
|
+
* Phase 2: Replace tool results with minimal one-line references.
|
|
69
|
+
* More aggressive than Phase 1. Modifies messages in-place.
|
|
70
|
+
*
|
|
71
|
+
* @param boundary - Only process messages at indices [0, boundary)
|
|
72
|
+
* @returns Number of results compacted
|
|
73
|
+
*/
|
|
74
|
+
export declare function compactToReferences(messages: PiMessage[], boundary: number, opts: {
|
|
75
|
+
preserveToolNames: string[];
|
|
76
|
+
}): number;
|
|
77
|
+
/**
|
|
78
|
+
* Build a rolling summarization prompt.
|
|
79
|
+
*
|
|
80
|
+
* Unlike a one-shot summarizer that truncates messages, this takes:
|
|
81
|
+
* - The existing rolling context (from previous summarization cycles)
|
|
82
|
+
* - A batch of messages IN FULL (no truncation)
|
|
83
|
+
*
|
|
84
|
+
* By the time Phase 3 runs, tool results have already been compacted by
|
|
85
|
+
* Phase 1+2, so the messages are mostly assistant reasoning + user text +
|
|
86
|
+
* one-liner tool references. Sending them in full preserves all information.
|
|
87
|
+
*
|
|
88
|
+
* @param rollingContext - Existing summary from previous cycles ("" if first time)
|
|
89
|
+
* @param messages - Full message array
|
|
90
|
+
* @param keepLastN - Number of recent messages to exclude from summarization
|
|
91
|
+
* @returns The summarization prompt, or empty string if nothing to summarize
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildSummarizationPrompt(rollingContext: string, messages: PiMessage[], keepLastN: number): string;
|
|
94
|
+
/**
|
|
95
|
+
* Apply the summarization result: replace old messages with a single
|
|
96
|
+
* user message containing the summary, keeping the last N messages raw.
|
|
97
|
+
*/
|
|
98
|
+
export declare function applySummarization(messages: PiMessage[], summary: string, keepLastN: number): void;
|
|
99
|
+
/**
|
|
100
|
+
* Apply context compaction in cascading phases.
|
|
101
|
+
*
|
|
102
|
+
* Called at the start of processUserMessage(), BEFORE pushing the new user
|
|
103
|
+
* message. At that point every message in the array is from previous
|
|
104
|
+
* user-request cycles — the model has already read and acted on those tool
|
|
105
|
+
* results, so they are safe to compact.
|
|
106
|
+
*
|
|
107
|
+
* Within a single user-request cycle (U -> tool calls -> R), all tool
|
|
108
|
+
* outputs remain at full fidelity so the model can reason correctly.
|
|
109
|
+
*
|
|
110
|
+
* @param lastCompactedIndex - Index up to which Phase 1 was already applied
|
|
111
|
+
* in a previous call. Phase 1 only scans [lastCompactedIndex, messages.length).
|
|
112
|
+
* Phase 2 and 3 scan the full range but naturally skip already-compact results.
|
|
113
|
+
* @param rollingContext - Existing rolling summary from previous Phase 3 cycles.
|
|
114
|
+
*/
|
|
115
|
+
export declare function compactContext(messages: PiMessage[], systemPrompt: string, config: CompactionConfig, lastCompactedIndex?: number, rollingContext?: string): CompactionResult;
|
|
116
|
+
//# sourceMappingURL=pi-context-compactor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_COMPACTION_CONFIG={contextWindowTokens:128e3,maskCharThreshold:3e3,maskHeadChars:500,referenceThresholdRatio:.6,summarizationThresholdRatio:.9,keepLastNMessages:6,preserveToolNames:["memory_search","search_memories"]};export function estimateSessionTokens(e,t){let n=t.length;for(const t of e)if("user"===t.role)if("string"==typeof t.content)n+=t.content.length;else for(const e of t.content)"text"===e.type&&(n+=e.text.length),"image"===e.type&&(n+=4e3);else if("assistant"===t.role)for(const e of t.content)"text"===e.type&&(n+=e.text.length),"thinking"===e.type&&(n+=e.thinking.length),"toolCall"===e.type&&(n+=JSON.stringify(e.arguments).length+e.name.length);else if("toolResult"===t.role)for(const e of t.content)"text"===e.type&&(n+=e.text.length),"image"===e.type&&(n+=4e3);return Math.ceil(n/4)}function e(e,t){return t.some(t=>e===t||e.includes(t))}function t(e){return e.content.filter(e=>"text"===e.type).map(e=>e.text).join("\n")}function n(e){return t(e).length}function o(e,t,n){for(let o=n-1;o>=0;o--){const n=e[o];if("assistant"===n.role)for(const e of n.content)if("toolCall"===e.type&&e.id===t)return{name:e.name,args:e.arguments}}return null}function s(e,t,n){if(!t)return`[${e}: ${n} chars]`;switch(e){case"Read":{const e=t.file_path||t.path||"unknown",n=t.offset??1,o=t.limit;return`[Read: ${e}${o?`, lines ${n}-${Number(n)+Number(o)}`:""}]`}case"Grep":return`[Grep: "${t.pattern||"?"}" in ${t.path||"."}]`;case"Glob":return`[Glob: "${t.pattern||"?"}"]`;case"Bash":{const e=t.command||"?";return`[Bash: ${e.length>80?e.slice(0,77)+"...":e}]`}default:{const o=Object.values(t).find(e=>"string"==typeof e),s=o?o.slice(0,60):"";return`[${e}${s?": "+s:""}, ${n} chars]`}}}export function maskOldObservations(r,a,i,l){let c=0;for(let u=a;u<i&&u<r.length;u++){const a=r[u];if("toolResult"!==a.role)continue;if(e(a.toolName,l.preserveToolNames))continue;const i=n(a);if(i<=l.charThreshold)continue;const m=o(r,a.toolCallId,u),h=`${s(a.toolName,m?.args,i)}\n${t(a).slice(0,l.headChars)}\n... [truncated — ${i} chars total. Re-run tool to see full content]`;a.content=[{type:"text",text:h}],c++}return c}function r(e,t,n,o){if(n){return`[${e}] Error: ${o.slice(0,100)}`}if(!t)return`[${e}] Completed. Re-run to see results.`;switch(e){case"Read":return`[Read: ${t.file_path||t.path||"unknown"}] Content available via Read tool`;case"Grep":return`[Grep: "${t.pattern||"?"}" in ${t.path||"."}] ~${(o.match(/\n/g)||[]).length} matches. Re-run to see results`;case"Glob":return`[Glob: "${t.pattern||"?"}"] ~${(o.match(/\n/g)||[]).length} files matched. Re-run to see results`;case"Bash":{const e=t.command||"?",n=e.length>80?e.slice(0,77)+"...":e,s=o.match(/exit code[:\s]*(\d+)/i);return`[Bash: ${n}] Exit code: ${s?s[1]:"0"}`}case"Write":case"Edit":return`[${e}: ${t.file_path||t.path||"unknown"}] Success`;default:{const n=Object.values(t).find(e=>"string"==typeof e),o=n?n.slice(0,60):"";return`[${e}${o?": "+o:""}] Completed. Re-run to see results`}}}export function compactToReferences(s,a,i){let l=0;for(let c=0;c<a&&c<s.length;c++){const a=s[c];if("toolResult"!==a.role)continue;if(e(a.toolName,i.preserveToolNames))continue;if(n(a)<200)continue;const u=o(s,a.toolCallId,c),m=t(a),h=r(a.toolName,u?.args,a.isError,m);a.content=[{type:"text",text:h}],l++}return l}export function buildSummarizationPrompt(e,n,o){const s=Math.max(0,n.length-o),r=n.slice(0,s);if(0===r.length)return"";const a=[];for(const e of r)if("user"===e.role){const t="string"==typeof e.content?e.content:e.content.filter(e=>"text"===e.type).map(e=>e.text).join("\n");a.push(`USER: ${t}`)}else if("assistant"===e.role){const t=e.content.filter(e=>"text"===e.type).map(e=>e.text),n=e.content.filter(e=>"toolCall"===e.type).map(e=>`${e.name}(${JSON.stringify(e.arguments).slice(0,200)})`);t.length&&a.push(`ASSISTANT: ${t.join("\n")}`),n.length&&a.push(`TOOLS CALLED: ${n.join(", ")}`)}else if("toolResult"===e.role){const n=t(e);a.push(`TOOL RESULT [${e.toolName}]: ${n}`)}return`${e?`<existing_summary>\n${e}\n</existing_summary>\n\n`:""}Integrate the following conversation into ${e?"an updated":"a"} structured summary. Preserve all important details needed to continue the conversation. Be concise but do not lose critical information.\n\n<conversation>\n${a.join("\n")}\n</conversation>\n\nProvide the summary in this exact format:\n\n## Conversation Summary\n\n### Task & Intent\n[What the user is trying to accomplish]\n\n### Files Modified\n[List of files created, modified, or read with their current state]\n\n### Key Decisions & Errors\n[Important decisions made, errors encountered, approaches tried and discarded]\n\n### Current State\n[Where we are in the task, what remains to be done]`}export function applySummarization(e,t,n){const o=Math.max(0,e.length-n);if(o<=0)return;const s=e.slice(o),r={role:"user",content:`[Context Summary]\n\n${t}`,timestamp:Date.now()};e.length=0,e.push(r,...s)}export function compactContext(e,t,n,o=0,s=""){const r=e.length,a={phase:0,estimatedTokensBefore:estimateSessionTokens(e,t),estimatedTokensAfter:0,maskedResults:0,compactedResults:0,summarized:!1};if(0===r)return a.estimatedTokensAfter=a.estimatedTokensBefore,a;const i=maskOldObservations(e,o,r,{charThreshold:n.maskCharThreshold,headChars:n.maskHeadChars,preserveToolNames:n.preserveToolNames});i>0&&(a.phase=1,a.maskedResults=i);let l=estimateSessionTokens(e,t);if(l>n.contextWindowTokens*n.referenceThresholdRatio){const t=compactToReferences(e,r,{preserveToolNames:n.preserveToolNames});t>0&&(a.phase=2,a.compactedResults=t)}l=estimateSessionTokens(e,t);if(l>n.contextWindowTokens*n.summarizationThresholdRatio){const t=buildSummarizationPrompt(s,e,n.keepLastNMessages);t&&(a.phase=3,a.summarized=!0,a.summarizationPrompt=t)}return a.estimatedTokensAfter=estimateSessionTokens(e,t),a}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Bridge — connects Hera's MCP tool servers to the Pi Agent tool registry.
|
|
3
|
+
*
|
|
4
|
+
* The Claude Agent SDK natively understands MCP servers and routes tool calls
|
|
5
|
+
* to them. The Pi Agent provider doesn't have this built-in, so this bridge
|
|
6
|
+
* extracts tool definitions from MCP servers and creates tool executors that
|
|
7
|
+
* route calls back through the MCP transport.
|
|
8
|
+
*
|
|
9
|
+
* This module handles:
|
|
10
|
+
* 1. Extracting tool schemas from MCP SDK server instances
|
|
11
|
+
* 2. Routing tool execution through the MCP transport
|
|
12
|
+
* 3. Converting between MCP result format and pi-ai tool result format
|
|
13
|
+
*/
|
|
14
|
+
import { ToolRegistry, type PiTool, type PiToolResult } from "./pi-tool-adapter.js";
|
|
15
|
+
import type { PiQueryOptions } from "./pi-types.js";
|
|
16
|
+
/**
|
|
17
|
+
* Extract tool definitions from an MCP SDK server instance.
|
|
18
|
+
*
|
|
19
|
+
* MCP SDK servers created with createSdkMcpServer() have an internal
|
|
20
|
+
* structure we can introspect to get tool names, descriptions, and schemas.
|
|
21
|
+
* The exact method depends on the SDK version — we try multiple approaches.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractToolsFromMcpServer(serverName: string, serverInstance: unknown): Promise<PiTool[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Execute a tool call through an MCP server.
|
|
26
|
+
*
|
|
27
|
+
* Routes the call to the correct server based on the tool name prefix
|
|
28
|
+
* (mcp__<serverName>__<toolName>).
|
|
29
|
+
*/
|
|
30
|
+
export declare function executeMcpTool(fullToolName: string, toolCallId: string, args: Record<string, unknown>, mcpServers: Record<string, unknown>): Promise<PiToolResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Create a fully configured ToolRegistry from Hera's query options.
|
|
33
|
+
*
|
|
34
|
+
* This extracts tools from all MCP servers, adds built-in tools,
|
|
35
|
+
* and sets up executors that route calls to the right place.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createToolRegistryFromOptions(options: PiQueryOptions): Promise<ToolRegistry>;
|
|
38
|
+
//# sourceMappingURL=pi-mcp-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ToolRegistry as t,BUILTIN_TOOL_DEFINITIONS as e}from"./pi-tool-adapter.js";import{executeBuiltinTool as o}from"./pi-tool-executor.js";export async function extractToolsFromMcpServer(t,e){const o=[];if(!e||"object"!=typeof e)return o;const r=e;if("function"==typeof r.getTools)try{const e=await r.getTools();for(const r of e)o.push({name:`mcp__${t}__${r.name}`,description:r.description||"",parameters:r.inputSchema||r.parameters||{type:"object",properties:{}}});return o}catch{}if(r.tools&&Array.isArray(r.tools)){for(const e of r.tools)o.push({name:`mcp__${t}__${e.name}`,description:e.description||"",parameters:e.inputSchema||e.parameters||{type:"object",properties:{}}});return o}if(r._tools&&"object"==typeof r._tools){const e=r._tools,n=e instanceof Map?Array.from(e.entries()):Object.entries(e);for(const[e,r]of n)o.push({name:`mcp__${t}__${e}`,description:r.description||"",parameters:r.inputSchema||r.parameters||{type:"object",properties:{}}});return o}if("function"==typeof r.handleRequest)try{const e=await r.handleRequest({method:"tools/list",params:{}});if(e?.tools&&Array.isArray(e.tools)){for(const r of e.tools)o.push({name:`mcp__${t}__${r.name}`,description:r.description||"",parameters:r.inputSchema||{type:"object",properties:{}}});return o}}catch{}if(r.instance&&"object"==typeof r.instance)return extractToolsFromMcpServer(t,r.instance);if(r.server&&"object"==typeof r.server){const e=r.server._requestHandlers;if(e instanceof Map&&e.has("tools/list"))try{const r=e.get("tools/list"),n=await r({method:"tools/list",params:{}});if(n?.tools&&Array.isArray(n.tools)){for(const e of n.tools)o.push({name:`mcp__${t}__${e.name}`,description:e.description||"",parameters:e.inputSchema||{type:"object",properties:{}}});return o}}catch{}}return o}export async function executeMcpTool(t,e,o,n){const s=t.split("__");if(s.length<3||"mcp"!==s[0])return{content:[{type:"text",text:`Invalid MCP tool name: ${t}`}],isError:!0};const c=s[1],i=s.slice(2).join("__"),a=n[c];if(!a)return{content:[{type:"text",text:`MCP server not found: ${c}`}],isError:!0};const p=a;if("function"==typeof p.handleRequest)try{return r(await p.handleRequest({method:"tools/call",params:{name:i,arguments:o}}))}catch(t){return{content:[{type:"text",text:`MCP tool execution error: ${t}`}],isError:!0}}if("function"==typeof p.executeTool)try{return r(await p.executeTool(i,o))}catch(t){return{content:[{type:"text",text:`MCP tool execution error: ${t}`}],isError:!0}}if(p.instance)return executeMcpTool(t,e,o,{[c]:p.instance});if(p.server&&"object"==typeof p.server){const t=p.server._requestHandlers;if(t instanceof Map&&t.has("tools/call"))try{const e=t.get("tools/call");return r(await e({method:"tools/call",params:{name:i,arguments:o}}))}catch(t){return{content:[{type:"text",text:`MCP tool execution error: ${t}`}],isError:!0}}}return{content:[{type:"text",text:`Cannot execute tool on MCP server ${c}: no known execution method`}],isError:!0}}function r(t){if(!t)return{content:[{type:"text",text:"(no response)"}],isError:!1};const e=!0===t.isError;if(t.content&&Array.isArray(t.content)){return{content:t.content.map(t=>"text"===t.type?{type:"text",text:t.text||""}:"image"===t.type?{type:"image",data:t.data||"",mimeType:t.mimeType||"image/png"}:{type:"text",text:JSON.stringify(t)}),isError:e}}return"string"==typeof t?{content:[{type:"text",text:t}],isError:e}:{content:[{type:"text",text:JSON.stringify(t,null,2)}],isError:e}}export async function createToolRegistryFromOptions(r){const n=new t;n.registerTools(e);const s=r.mcpServers??{};for(const[t,e]of Object.entries(s))try{const o=await extractToolsFromMcpServer(t,e);o.length>0?(n.registerTools(o),console.log(`[PiAgent] Registered ${o.length} tools from MCP server "${t}": ${o.map(t=>t.name).join(", ")}`)):console.warn(`[PiAgent] MCP server "${t}" exposed 0 tools (extraction found nothing)`)}catch(e){console.error(`[PiAgent] Failed to extract tools from MCP server ${t}: ${e}`)}return r.canUseTool&&n.setPermissionChecker(r.canUseTool),n.setExecutor(async(t,e,n)=>{if(t.startsWith("mcp__"))return executeMcpTool(t,e,n,s);const c=await o(t,n,r.cwd);return c||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}}),n}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message format adapters between Hera's SDK-style messages and pi-ai's message types.
|
|
3
|
+
*
|
|
4
|
+
* Hera's SessionAgent uses Anthropic SDK message format (SDKUserMessage, SDKAssistantMessage).
|
|
5
|
+
* Pi-ai uses its own UserMessage/AssistantMessage/ToolResultMessage format.
|
|
6
|
+
* This module bridges the two.
|
|
7
|
+
*/
|
|
8
|
+
import type { SDKUserMessage, SDKAssistantMessage } from "./pi-types.js";
|
|
9
|
+
export interface PiTextContent {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
textSignature?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PiImageContent {
|
|
15
|
+
type: "image";
|
|
16
|
+
data: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PiThinkingContent {
|
|
20
|
+
type: "thinking";
|
|
21
|
+
thinking: string;
|
|
22
|
+
thinkingSignature?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PiToolCall {
|
|
25
|
+
type: "toolCall";
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
arguments: Record<string, any>;
|
|
29
|
+
thoughtSignature?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface PiUsage {
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
cacheRead: number;
|
|
35
|
+
cacheWrite: number;
|
|
36
|
+
totalTokens: number;
|
|
37
|
+
cost: {
|
|
38
|
+
input: number;
|
|
39
|
+
output: number;
|
|
40
|
+
cacheRead: number;
|
|
41
|
+
cacheWrite: number;
|
|
42
|
+
total: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface PiUserMessage {
|
|
46
|
+
role: "user";
|
|
47
|
+
content: string | (PiTextContent | PiImageContent)[];
|
|
48
|
+
timestamp: number;
|
|
49
|
+
}
|
|
50
|
+
export interface PiAssistantMessage {
|
|
51
|
+
role: "assistant";
|
|
52
|
+
content: (PiTextContent | PiThinkingContent | PiToolCall)[];
|
|
53
|
+
api: string;
|
|
54
|
+
provider: string;
|
|
55
|
+
model: string;
|
|
56
|
+
usage: PiUsage;
|
|
57
|
+
stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
|
|
58
|
+
errorMessage?: string;
|
|
59
|
+
timestamp: number;
|
|
60
|
+
}
|
|
61
|
+
export interface PiToolResultMessage {
|
|
62
|
+
role: "toolResult";
|
|
63
|
+
toolCallId: string;
|
|
64
|
+
toolName: string;
|
|
65
|
+
content: (PiTextContent | PiImageContent)[];
|
|
66
|
+
details?: any;
|
|
67
|
+
isError: boolean;
|
|
68
|
+
timestamp: number;
|
|
69
|
+
}
|
|
70
|
+
export type PiMessage = PiUserMessage | PiAssistantMessage | PiToolResultMessage;
|
|
71
|
+
/**
|
|
72
|
+
* Convert an SDK-format user message (from Hera's MessageQueue) to a pi-ai UserMessage.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sdkUserToPiUser(sdkMsg: SDKUserMessage): PiUserMessage;
|
|
75
|
+
/**
|
|
76
|
+
* Convert a pi-ai AssistantMessage to the SDK format that SessionAgent.processOutput() expects.
|
|
77
|
+
*/
|
|
78
|
+
export declare function piAssistantToSdk(piMsg: PiAssistantMessage): SDKAssistantMessage;
|
|
79
|
+
/**
|
|
80
|
+
* Convert pi-ai Usage to the SDK's modelUsage format for result messages.
|
|
81
|
+
*/
|
|
82
|
+
export declare function piUsageToModelUsage(modelId: string, usage: PiUsage): Record<string, {
|
|
83
|
+
inputTokens: number;
|
|
84
|
+
outputTokens: number;
|
|
85
|
+
cacheReadInputTokens: number;
|
|
86
|
+
cacheCreationInputTokens: number;
|
|
87
|
+
costUSD: number;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Extract plain text from an SDK assistant message's content blocks.
|
|
91
|
+
*/
|
|
92
|
+
export declare function extractTextFromSdkAssistant(msg: SDKAssistantMessage): string;
|
|
93
|
+
//# sourceMappingURL=pi-message-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function sdkUserToPiUser(t){const e=t.message.content;if("string"==typeof e)return{role:"user",content:e,timestamp:Date.now()};const n=[];for(const t of e)"text"===t.type?n.push({type:"text",text:t.text}):"image"===t.type&&n.push({type:"image",data:t.source.data,mimeType:t.source.media_type});return{role:"user",content:n.length>0?n:"",timestamp:Date.now()}}export function piAssistantToSdk(e){const n=[];for(const t of e.content)"text"===t.type?n.push({type:"text",text:t.text}):"toolCall"===t.type&&n.push({type:"tool_use",id:t.id,name:t.name,input:t.arguments});return{type:"assistant",message:{content:n,model:e.model,role:"assistant",stop_reason:t(e.stopReason),usage:{input_tokens:e.usage.input,output_tokens:e.usage.output,cache_read_input_tokens:e.usage.cacheRead,cache_creation_input_tokens:e.usage.cacheWrite}}}}function t(t){switch(t){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return t}}export function piUsageToModelUsage(t,e){return{[t]:{inputTokens:e.input,outputTokens:e.output,cacheReadInputTokens:e.cacheRead,cacheCreationInputTokens:e.cacheWrite,costUSD:e.cost.total}}}export function extractTextFromSdkAssistant(t){return t.message.content.filter(t=>"text"===t.type).map(t=>t.text).join("")}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Agent query() — Drop-in replacement for the Claude Agent SDK's query().
|
|
3
|
+
*
|
|
4
|
+
* This function replicates the exact interface of the Claude Agent SDK's query()
|
|
5
|
+
* function, but uses @mariozechner/pi-ai (stream/complete) under the hood.
|
|
6
|
+
*
|
|
7
|
+
* The key contract:
|
|
8
|
+
* - Accepts an AsyncIterable<UserMessage> as `prompt` (the MessageQueue)
|
|
9
|
+
* - Returns an AsyncIterable<SDKMessage> (the output stream)
|
|
10
|
+
* - The output stream yields: system(init), assistant, tool_progress, result messages
|
|
11
|
+
* - Has .interrupt(), .setModel(), .close(), .supportedModels() methods
|
|
12
|
+
*
|
|
13
|
+
* Architecture:
|
|
14
|
+
* - For each user message from the prompt iterable, runs a pi-ai agent loop
|
|
15
|
+
* - The agent loop handles tool calling internally
|
|
16
|
+
* - Results are mapped to SDK message format and yielded
|
|
17
|
+
* - Session state (conversation history) is maintained in-memory
|
|
18
|
+
*/
|
|
19
|
+
import type { SDKMessage, SDKUserMessage, PiQueryOptions, PiProviderConfig } from "./pi-types.js";
|
|
20
|
+
import { ToolRegistry } from "./pi-tool-adapter.js";
|
|
21
|
+
export interface PiQueryHandle extends AsyncIterable<SDKMessage> {
|
|
22
|
+
/** Interrupt the current processing. */
|
|
23
|
+
interrupt(): Promise<void>;
|
|
24
|
+
/** Change the model mid-session. */
|
|
25
|
+
setModel(model: string): Promise<void>;
|
|
26
|
+
/** Close the query handle. */
|
|
27
|
+
close(): void;
|
|
28
|
+
/** List supported models. */
|
|
29
|
+
supportedModels(): Promise<Array<{
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}>>;
|
|
33
|
+
}
|
|
34
|
+
export interface PiQueryInput {
|
|
35
|
+
prompt: AsyncIterable<SDKUserMessage>;
|
|
36
|
+
options: PiQueryOptions;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Create a Pi Agent query handle.
|
|
40
|
+
*
|
|
41
|
+
* This is the drop-in replacement for the Claude Agent SDK's `query()`.
|
|
42
|
+
* It consumes user messages from the prompt iterable, runs them through
|
|
43
|
+
* pi-ai's LLM API with tool execution, and yields SDK-compatible messages.
|
|
44
|
+
*
|
|
45
|
+
* IMPORTANT: Always pass a pre-built ToolRegistry (via createToolRegistryFromOptions)
|
|
46
|
+
* to ensure MCP tools are available. Without it, only built-in tools work.
|
|
47
|
+
*/
|
|
48
|
+
export declare function piQuery(input: PiQueryInput, providerConfig: PiProviderConfig, toolRegistry?: ToolRegistry): PiQueryHandle;
|
|
49
|
+
//# sourceMappingURL=pi-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{sdkUserToPiUser as e,piAssistantToSdk as t,extractTextFromSdkAssistant as o}from"./pi-message-adapter.js";import{ToolRegistry as s,BUILTIN_TOOL_DEFINITIONS as n}from"./pi-tool-adapter.js";import{executeBuiltinTool as r}from"./pi-tool-executor.js";import{createToolRegistryFromOptions as a}from"./pi-mcp-bridge.js";import{buildSkillsAndCommandsBlock as i}from"./pi-skill-loader.js";import{compactContext as c,applySummarization as l,DEFAULT_COMPACTION_CONFIG as d}from"./pi-context-compactor.js";import{randomUUID as u}from"node:crypto";let m=null;async function p(){if(!m)try{const e=await import("@mariozechner/pi-ai");m={stream:e.stream,complete:e.complete,getModel:e.getModel,getModels:e.getModels,getProviders:e.getProviders}}catch(e){throw new Error(`Failed to load @mariozechner/pi-ai. Install it with: npm install @mariozechner/pi-ai\n${e}`)}return m}const g=new Map;const h=setInterval(function(){const e=Date.now();for(const[t,o]of g)e-o.lastAccessTime>864e5&&g.delete(t)},6e5);"function"==typeof h.unref&&h.unref();export function piQuery(m,h,k){const{prompt:T,options:b}=m;let _=new AbortController,v=!1,P=!1,$=h.modelId,C=h.provider;const M=b.resume||u();let R=g.get(M);if(!R){let e="";if("string"==typeof b.systemPrompt)e=b.systemPrompt;else if(b.systemPrompt&&"object"==typeof b.systemPrompt){const t=b.systemPrompt.preset,o=b.systemPrompt.append||"";e="claude_code"===t?"You are an AI coding assistant. You have access to tools for reading, writing, and editing files, running shell commands, searching code, and more. Use these tools to help the user with their coding tasks.\n\nKey behaviors:\n- Read files before editing them\n- Use Grep and Glob to explore the codebase\n- Run tests after making changes\n- Be thorough but concise in explanations\n- When writing code, follow existing patterns and conventions in the codebase\n\n"+o:o}R={id:M,messages:[],systemPrompt:e,totalCostUsd:0,totalInputTokens:0,totalOutputTokens:0,totalTurns:0,startTime:Date.now(),lastAccessTime:Date.now(),lastCompactedIndex:0,compactionCount:0,rollingContext:""},g.set(M,R)}R.lastAccessTime=Date.now();const I=k??new s;if(k||(I.registerTools(n),b.mcpServers&&Object.keys(b.mcpServers).length>0&&a(b).then(e=>{for(const t of e.getTools())t.name.startsWith("mcp__")&&I.registerTool(t)}).catch(e=>{console.error(`[PiAgent] Failed to bridge MCP tools: ${e}`)})),b.canUseTool&&I.setPermissionChecker(b.canUseTool),!k){const e=b.mcpServers??{};I.setExecutor(async(t,o,s)=>{if(t.startsWith("mcp__")){const{executeMcpTool:n}=await import("./pi-mcp-bridge.js");return n(t,o,s,e)}const n=await r(t,s,b.cwd,M);return n||{content:[{type:"text",text:`Unknown tool: ${t}`}],isError:!0}})}async function*W(s){if(P)return;let n=R.systemPrompt;const r=i(b.cwd||"");if(r&&(n+="\n\n"+r),R.messages.length>0){const e=c(R.messages,n,{...d,contextWindowTokens:h.contextWindowTokens??128e3},R.lastCompactedIndex,R.rollingContext);if(e.phase>0&&(R.compactionCount++,console.log(`[pi-query] Context compacted (cycle #${R.compactionCount}): phase=${e.phase}, tokens ${e.estimatedTokensBefore}->${e.estimatedTokensAfter}, masked=${e.maskedResults}, compacted=${e.compactedResults}`)),e.summarized&&e.summarizationPrompt)try{const t=await p();if(t){const o={...d,contextWindowTokens:h.contextWindowTokens??128e3};let s=C,n=$;if(o.summarizationModel){const e=o.summarizationModel.split("/",2);2===e.length?(s=e[0],n=e[1]):n=e[0]}const r=t.getModel(s,n),a=t.stream(r,{systemPrompt:"You are a conversation summarizer. Be concise and structured.",messages:[{role:"user",content:e.summarizationPrompt,timestamp:Date.now()}]},{...h.apiKey?{apiKey:h.apiKey}:{},...h.headers?{headers:h.headers}:{},temperature:.3,maxTokens:1024});for await(const e of a);const i=await a.result(),c=i.content?.filter(e=>"text"===e.type).map(e=>e.text).join("\n");c&&(R.rollingContext=c,l(R.messages,c,d.keepLastNMessages),console.log(`[pi-query] Phase 3 rolling summary applied, messages reduced to ${R.messages.length}`))}}catch(e){console.warn(`[pi-query] Phase 3 summarization failed (non-fatal): ${e}`)}R.lastCompactedIndex=R.messages.length}const a=e(s);R.messages.push(a),R.lastAccessTime=Date.now();const u=b.maxTurns??25;let m=0,g="",k="end_turn",T={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};const M=Date.now();for(;m<u&&!P&&!v;){m++;const e=I.getFilteredTools(b.allowedTools,b.disallowedTools);let s,r;try{s=await p()}catch(e){return void(yield y(R,"error_during_execution",g,k,$,M,T,[`${e}`]))}try{r=s.getModel(C,$)}catch(e){return void(yield y(R,"error_during_execution",g,k,$,M,T,[`Model not found: ${C}/${$}. ${e}`]))}const a={systemPrompt:n,messages:R.messages.map(x),tools:e.length>0?e:void 0},i={signal:_.signal};let c;h.apiKey&&(i.apiKey=h.apiKey),void 0!==h.temperature&&(i.temperature=h.temperature),void 0!==h.maxTokens&&(i.maxTokens=h.maxTokens),h.headers&&(i.headers=h.headers),h.reasoning&&"off"!==h.reasoning&&(i.reasoning=h.reasoning);try{const e=s.stream(r,a,i);for await(const t of e)if(P||v||_.signal.aborted)break;if("function"!=typeof e.result)throw new Error("Stream did not return a .result() method — ensure @mariozechner/pi-ai is correctly installed");c=await e.result()}catch(e){return v||_.signal.aborted?(v=!1,void(yield y(R,"error_during_execution",g,k,$,M,T,["aborted"]))):void(yield y(R,"error_during_execution",g,k,$,M,T,[`${e}`]))}w(T,c.usage),k=f(c.stopReason);const l=t(c);yield l,R.messages.push(c);const d=o(l);if(d&&(g=d),"toolUse"!==c.stopReason)break;const u=c.content.filter(e=>"toolCall"===e.type);if(0===u.length)break;for(const e of u){if(P||v||_.signal.aborted)break;const t=await I.checkPermission(e.name,e.arguments);if("deny"===t.behavior){const o={role:"toolResult",toolCallId:e.id,toolName:e.name,content:[{type:"text",text:`Permission denied: ${t.message}`}],isError:!0,timestamp:Date.now()};R.messages.push(o);continue}const o="allow"===t.behavior&&t.updatedInput?t.updatedInput:e.arguments,s=Date.now();let n;try{n=await I.execute(e.name,e.id,o)}catch(e){n={content:[{type:"text",text:`Tool execution error: ${e}`}],isError:!0}}const r=(Date.now()-s)/1e3;yield{type:"tool_progress",tool_name:e.name,elapsed_time_seconds:r};const a={role:"toolResult",toolCallId:e.id,toolName:e.name,content:n.content,isError:n.isError,timestamp:Date.now()};R.messages.push(a)}if(P||v||_.signal.aborted)return v=!1,void(yield y(R,"error_during_execution",g,k,$,M,T,["aborted"]))}m>=u&&!P?yield y(R,"error_max_turns",g,k,$,M,T):(v=!1,yield y(R,"success",g,k,$,M,T))}const D=async function*(){yield{type:"system",subtype:"init",slash_commands:[],session_id:M};try{for await(const e of T){if(P)break;(v||_.signal.aborted)&&(v=!1,_=new AbortController);for await(const t of W(e)){if(P)break;yield t}}}catch(e){P||(yield{type:"result",subtype:"error_during_execution",session_id:M,result:"",errors:[`${e}`]})}}();return{[Symbol.asyncIterator]:()=>D,async interrupt(){v=!0,_.abort()},async setModel(e){if(e.includes("/")){const[t,o]=e.split("/",2);C=t,$=o}else $=e},close(){P=!0,_.abort(),g.delete(M)},async supportedModels(){try{const e=await p();if(!e)return[];const t=e.getProviders(),o=[];for(const s of t)try{const t=e.getModels(s);for(const e of t)o.push({id:`${s}/${e.id}`,name:e.name||e.id})}catch{}return o}catch{return[]}}}}function f(e){switch(e){case"stop":return"end_turn";case"length":return"max_tokens";case"toolUse":return"tool_use";case"error":return"error";case"aborted":return"aborted";default:return e}}function y(e,t,o,s,n,r,a,i){const c=Date.now()-r;return e.totalCostUsd+=a.cost.total,e.totalTurns++,{type:"result",subtype:t,session_id:e.id,result:"success"===t?o:void 0,stop_reason:"success"===t?s:null,total_cost_usd:a.cost.total,duration_ms:c,num_turns:1,modelUsage:{[n]:{inputTokens:a.input,outputTokens:a.output,cacheReadInputTokens:a.cacheRead,cacheCreationInputTokens:a.cacheWrite,costUSD:a.cost.total}},...i?{errors:i}:{}}}function w(e,t){e.input+=t.input,e.output+=t.output,e.cacheRead+=t.cacheRead,e.cacheWrite+=t.cacheWrite,e.totalTokens+=t.totalTokens,e.cost.input+=t.cost.input,e.cost.output+=t.cost.output,e.cost.cacheRead+=t.cost.cacheRead,e.cost.cacheWrite+=t.cost.cacheWrite,e.cost.total+=t.cost.total}function x(e){return e}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Skill & Command Loader
|
|
3
|
+
*
|
|
4
|
+
* Discovers installed skills and custom commands from the workspace
|
|
5
|
+
* and builds a text block to append to the pi-agent system prompt.
|
|
6
|
+
*
|
|
7
|
+
* This compensates for the pi engine not having the built-in Skill tool
|
|
8
|
+
* that the Claude Agent SDK provides.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Build the skills and commands block to append to the pi-agent system prompt.
|
|
12
|
+
* Returns an empty string if no skills or commands are found.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildSkillsAndCommandsBlock(workspacePath: string): string;
|
|
15
|
+
//# sourceMappingURL=pi-skill-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as t,readdirSync as n,existsSync as e,statSync as i}from"fs";import{join as o,relative as s,extname as c}from"path";function r(t){const n={};if(!t.startsWith("---"))return n;const e=t.indexOf("\n---",3);if(-1===e)return n;const i=t.slice(4,e);for(const t of i.split("\n")){const e=t.indexOf(":");if(-1===e)continue;const i=t.slice(0,e).trim();let o=t.slice(e+1).trim();(o.startsWith('"')&&o.endsWith('"')||o.startsWith("'")&&o.endsWith("'"))&&(o=o.slice(1,-1)),n[i]=o}return n}function a(t){const s=[];if(!e(t))return s;let r;try{r=n(t)}catch{return s}for(const n of r){const e=o(t,n);try{const t=i(e);t.isDirectory()?s.push(...a(e)):t.isFile()&&".md"===c(n)&&s.push(e)}catch{continue}}return s}function l(n){const i=o(n,".claude","commands");if(!e(i))return[];const c=a(i),l=[];for(const e of c){let o;try{o=t(e,"utf-8")}catch{continue}const c=r(o).description||"",a="/"+s(i,e).replace(/\.md$/,"").replace(/\\/g,"/"),u=s(n,e);l.push({commandName:a,description:c,relativePath:u})}return l}export function buildSkillsAndCommandsBlock(i){if(!i)return"";const c=function(i){const c=o(i,".claude","skills");if(!e(c))return[];const a=[];let l;try{l=n(c)}catch{return[]}for(const n of l){const l=o(c,n,"SKILL.md");if(!e(l))continue;let u;try{u=t(l,"utf-8")}catch{continue}const f=r(u);if("false"===f["user-invocable"])continue;const m=f.name||n,d=f.description||"",h=s(i,l);a.push({name:m,description:d,relativePath:h})}return a}(i),a=l(i);if(0===c.length&&0===a.length)return"";const u=[];if(c.length>0){const t=["## Installed Skills","","Skills are available in `.claude/skills/`. To use a skill, read its SKILL.md file first for instructions.",""];for(const n of c)t.push(`- **${n.name}**${n.description?": "+n.description:""} → \`${n.relativePath}\``);u.push(t.join("\n"))}if(a.length>0){const t=["## Custom Commands","","Custom commands are available in `.claude/commands/`. To execute a command, read its .md file for the prompt template.",""];for(const n of a)t.push(`- **${n.commandName}**${n.description?": "+n.description:""} → \`${n.relativePath}\``);u.push(t.join("\n"))}return u.join("\n\n")}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool adapter: bridges MCP tool servers to pi-ai AgentTools.
|
|
3
|
+
*
|
|
4
|
+
* The Claude Agent SDK uses MCP servers for tools. Pi-ai uses AgentTool objects
|
|
5
|
+
* with inline execute() functions. This adapter extracts tool definitions from
|
|
6
|
+
* MCP servers and creates pi-ai compatible AgentTools that route execution
|
|
7
|
+
* back through the MCP transport.
|
|
8
|
+
*
|
|
9
|
+
* Since we don't have direct access to MCP server internals from this library,
|
|
10
|
+
* tools are registered declaratively and executed via a callback mechanism.
|
|
11
|
+
*/
|
|
12
|
+
import type { PiTextContent, PiImageContent } from "./pi-message-adapter.js";
|
|
13
|
+
/**
|
|
14
|
+
* Simplified Tool definition compatible with pi-ai.
|
|
15
|
+
* Uses plain JSON Schema instead of TypeBox to avoid the dependency.
|
|
16
|
+
*/
|
|
17
|
+
export interface PiTool {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
parameters: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Tool execution result in pi-ai format.
|
|
24
|
+
*/
|
|
25
|
+
export interface PiToolResult {
|
|
26
|
+
content: (PiTextContent | PiImageContent)[];
|
|
27
|
+
isError: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Callback for executing a tool. The provider implements this to route
|
|
31
|
+
* tool calls to the appropriate MCP server or built-in handler.
|
|
32
|
+
*/
|
|
33
|
+
export type ToolExecutor = (toolName: string, toolCallId: string, args: Record<string, unknown>) => Promise<PiToolResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Callback for checking tool permissions via canUseTool.
|
|
36
|
+
*/
|
|
37
|
+
export type ToolPermissionChecker = (toolName: string, input: Record<string, unknown>) => Promise<{
|
|
38
|
+
behavior: "allow";
|
|
39
|
+
updatedInput: unknown;
|
|
40
|
+
} | {
|
|
41
|
+
behavior: "deny";
|
|
42
|
+
message: string;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Registry that holds tool definitions and routes execution.
|
|
46
|
+
* This is the bridge between the pi-ai agent loop and Hera's MCP infrastructure.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ToolRegistry {
|
|
49
|
+
private tools;
|
|
50
|
+
private executor;
|
|
51
|
+
private permissionChecker;
|
|
52
|
+
/**
|
|
53
|
+
* Register a tool definition.
|
|
54
|
+
*/
|
|
55
|
+
registerTool(tool: PiTool): void;
|
|
56
|
+
/**
|
|
57
|
+
* Register multiple tools at once.
|
|
58
|
+
*/
|
|
59
|
+
registerTools(tools: PiTool[]): void;
|
|
60
|
+
/**
|
|
61
|
+
* Set the tool executor callback.
|
|
62
|
+
*/
|
|
63
|
+
setExecutor(executor: ToolExecutor): void;
|
|
64
|
+
/**
|
|
65
|
+
* Set the permission checker callback.
|
|
66
|
+
*/
|
|
67
|
+
setPermissionChecker(checker: ToolPermissionChecker): void;
|
|
68
|
+
/**
|
|
69
|
+
* Get all registered tools as pi-ai Tool objects.
|
|
70
|
+
*/
|
|
71
|
+
getTools(): PiTool[];
|
|
72
|
+
/**
|
|
73
|
+
* Get a specific tool by name.
|
|
74
|
+
*/
|
|
75
|
+
getTool(name: string): PiTool | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Check if a tool is allowed to execute.
|
|
78
|
+
*/
|
|
79
|
+
checkPermission(toolName: string, input: Record<string, unknown>): Promise<{
|
|
80
|
+
behavior: "allow";
|
|
81
|
+
updatedInput: unknown;
|
|
82
|
+
} | {
|
|
83
|
+
behavior: "deny";
|
|
84
|
+
message: string;
|
|
85
|
+
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Execute a tool call.
|
|
88
|
+
*/
|
|
89
|
+
execute(toolName: string, toolCallId: string, args: Record<string, unknown>): Promise<PiToolResult>;
|
|
90
|
+
/**
|
|
91
|
+
* Check if a tool name matches any allowed tool pattern.
|
|
92
|
+
* Supports glob patterns like "mcp__server-tools__*".
|
|
93
|
+
*/
|
|
94
|
+
static matchesPattern(toolName: string, patterns: string[]): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Filter tools based on allowed/disallowed lists.
|
|
97
|
+
*/
|
|
98
|
+
getFilteredTools(allowedTools?: string[], disallowedTools?: string[]): PiTool[];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Built-in tools that the Claude Agent SDK provides.
|
|
102
|
+
* These need to be emulated in the pi-agent provider.
|
|
103
|
+
*/
|
|
104
|
+
export declare const BUILTIN_TOOL_DEFINITIONS: PiTool[];
|
|
105
|
+
//# sourceMappingURL=pi-tool-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class ToolRegistry{tools=new Map;executor=null;permissionChecker=null;registerTool(e){this.tools.set(e.name,e)}registerTools(e){for(const t of e)this.registerTool(t)}setExecutor(e){this.executor=e}setPermissionChecker(e){this.permissionChecker=e}getTools(){return Array.from(this.tools.values())}getTool(e){return this.tools.get(e)}async checkPermission(e,t){return this.permissionChecker?this.permissionChecker(e,t):{behavior:"allow",updatedInput:t}}async execute(e,t,r){return this.executor?this.executor(e,t,r):{content:[{type:"text",text:`No executor registered for tool: ${e}`}],isError:!0}}static matchesPattern(e,t){for(const r of t){if(r===e)return!0;if(r.endsWith("*")){const t=r.slice(0,-1);if(e.startsWith(t))return!0}}return!1}getFilteredTools(e,t){let r=this.getTools();return e&&e.length>0&&(r=r.filter(t=>ToolRegistry.matchesPattern(t.name,e))),t&&t.length>0&&(r=r.filter(e=>!ToolRegistry.matchesPattern(e.name,t))),r}}export const BUILTIN_TOOL_DEFINITIONS=[{name:"Read",description:"Read a file from the filesystem. Returns file content with line numbers.",parameters:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to the file"},offset:{type:"number",description:"Line number to start from"},limit:{type:"number",description:"Number of lines to read"}},required:["file_path"]}},{name:"Write",description:"Write content to a file, creating or overwriting it.",parameters:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to the file"},content:{type:"string",description:"Content to write"}},required:["file_path","content"]}},{name:"Edit",description:"Perform exact string replacements in a file.",parameters:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to the file"},old_string:{type:"string",description:"Text to replace"},new_string:{type:"string",description:"Replacement text"},replace_all:{type:"boolean",description:"Replace all occurrences"}},required:["file_path","old_string","new_string"]}},{name:"Bash",description:"Execute a bash command.",parameters:{type:"object",properties:{command:{type:"string",description:"The command to execute"},timeout:{type:"number",description:"Timeout in milliseconds"},description:{type:"string",description:"Description of the command"}},required:["command"]}},{name:"Glob",description:"Find files matching a glob pattern.",parameters:{type:"object",properties:{pattern:{type:"string",description:"Glob pattern"},path:{type:"string",description:"Directory to search in"}},required:["pattern"]}},{name:"Grep",description:"Search file contents using regex patterns.",parameters:{type:"object",properties:{pattern:{type:"string",description:"Regex pattern"},path:{type:"string",description:"File or directory to search"},glob:{type:"string",description:"Glob pattern to filter files"},output_mode:{type:"string",enum:["content","files_with_matches","count"]}},required:["pattern"]}},{name:"WebSearch",description:"Search the web for information.",parameters:{type:"object",properties:{query:{type:"string",description:"Search query"}},required:["query"]}},{name:"WebFetch",description:"Fetch and process content from a URL.",parameters:{type:"object",properties:{url:{type:"string",description:"URL to fetch"},prompt:{type:"string",description:"Prompt for processing content"}},required:["url","prompt"]}},{name:"AskUserQuestion",description:"Ask the user questions during execution.",parameters:{type:"object",properties:{questions:{type:"array",items:{type:"object",properties:{question:{type:"string"},header:{type:"string"},options:{type:"array",items:{type:"object",properties:{label:{type:"string"},description:{type:"string"}},required:["label"]}},multiSelect:{type:"boolean"}},required:["question","header","options","multiSelect"]}}},required:["questions"]}},{name:"TodoWrite",description:"Create and manage a structured task list.",parameters:{type:"object",properties:{todos:{type:"array",items:{type:"object",properties:{content:{type:"string"},status:{type:"string",enum:["pending","in_progress","completed"]},activeForm:{type:"string"}},required:["content","status","activeForm"]}}},required:["todos"]}}];
|