From 3b6af0bd1600f034b0466e1b8ff017fbabeba910 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:45:58 +0200 Subject: [PATCH] fix(server): Pi loads every selected skill without losing prompt text (#17194) --- .../message_steering/pi_transcript.ndjson | 2 - .../fixtures/multi_turn/pi_transcript.ndjson | 2 - .../pi_compaction/pi_transcript.ndjson | 2 - .../pi_transcript.ndjson | 4 - .../fixtures/simple/pi_transcript.ndjson | 2 - .../thread_rollback/pi_transcript.ndjson | 2 - .../pi_transcript.ndjson | 4 - .../pi_transcript.ndjson | 2 - .../provider-pi/src/server/adapter.test.ts | 62 +------- packages/provider-pi/src/server/adapter.ts | 39 +---- .../provider-pi/src/server/commands.test.ts | 17 -- packages/provider-pi/src/server/commands.ts | 35 ---- .../src/server/mcpExtensionSource.test.ts | 149 +++++++++++++++++- .../src/server/mcpExtensionSource.ts | 36 ++++- 14 files changed, 185 insertions(+), 173 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/pi_transcript.ndjson index 092ab40ad110f490170d9267d42183257723f021..32ffdd3987c4b7777722b79bb17a2ae44c327386 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/pi_transcript.ndjson @@ -1,10 +1,8 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"message_steering","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/pi_transcript.ndjson index 2e3fd92409bff75e9f4eb2e2be9e7fc77b1fec1f..7e32afc0a5b2ec6d272c3d54cccc5fd2d529b48e 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/pi_transcript.ndjson @@ -1,10 +1,8 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"multi_turn","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/pi_compaction/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/pi_compaction/pi_transcript.ndjson index ff20ad15dab35c7c03bcfa81afe19becf4682522..43aadfd2d881ae37914b346b753589cd02571ab0 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/pi_compaction/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/pi_compaction/pi_transcript.ndjson @@ -1,10 +1,8 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"pi_compaction","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/pi_transcript.ndjson index 06a20a5ceec4486efd91ac72133c7fefae0904fb..cba3bd8a396d17be4efba55582747dfb07b2b1ab 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/provider_thread_resume/pi_transcript.ndjson @@ -1,10 +1,8 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"provider_thread_resume","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} @@ -61,9 +59,7 @@ {"type":"emit_inbound","label":"response:get_session_stats","frame":{"id":"t3-8","type":"response","command":"get_session_stats","success":true,"data":{"sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","userMessages":1,"assistantMessages":1,"toolCalls":0,"toolResults":0,"totalMessages":3,"tokens":{"input":1792,"output":85,"cacheRead":0,"cacheWrite":0,"total":1877},"cost":0.00008212932000000001,"contextUsage":{"tokens":1877,"contextWindow":1048576,"percent":0.17900466918945312}}}} {"type":"expect_outbound","label":"process_start@p2","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"switch_session@p2","frame":{"type":"switch_session","sessionPath":"/pi-sessions/session-1.jsonl","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands@p2","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000003","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} -{"type":"emit_inbound","label":"response:get_commands@p2","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000004","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000005","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:switch_session@p2","frame":{"id":"t3-0","type":"response","command":"switch_session","success":true,"data":{"cancelled":false}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/pi_transcript.ndjson index 231a1cfdb08d0c54ce4c1a30afa0fd7b82c55fa7..ba7f84de2878843678725928703ce99a0aaccacb 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/simple/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/pi_transcript.ndjson @@ -1,10 +1,8 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"simple","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/pi_transcript.ndjson index f931122159525c047a701270632d613413d4ae82..2fc3def568e6b75415ea1d654adfde7e51858017 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback/pi_transcript.ndjson @@ -1,11 +1,9 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"thread_rollback","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} {"type":"emit_inbound","label":"response:get_entries","frame":{"id":"t3-3","type":"response","command":"get_entries","success":true,"data":{"entries":[{"type":"model_change","id":"648fe89a","parentId":null,"timestamp":"2026-01-01T00:00:00.000Z","provider":"openrouter","modelId":"deepseek/deepseek-v4-flash"},{"type":"thinking_level_change","id":"00c31b78","parentId":"648fe89a","timestamp":"2026-01-01T00:00:00.000Z","thinkingLevel":"high"}],"leafId":"00c31b78"}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback_after_stop/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback_after_stop/pi_transcript.ndjson index 4bee5483ad4239f6528198b658000753cf9de488..2d92c45b2adbc98d7eb11fcda5c1304b911b57b5 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback_after_stop/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/thread_rollback_after_stop/pi_transcript.ndjson @@ -1,10 +1,8 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"thread_rollback_after_stop","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} @@ -151,9 +149,7 @@ {"type":"emit_inbound","label":"response:get_entries","frame":{"id":"t3-10","type":"response","command":"get_entries","success":true,"data":{"entries":[{"type":"message","id":"ee73daae","parentId":"b184b5a8","timestamp":"2026-01-01T00:00:01.971Z","message":{"role":"user","content":[{"type":"text","text":"Run this exact local command: `node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"`. Do not answer until it completes, then respond exactly: interrupt fixture should not finish naturally."}],"timestamp":1767225601969}},{"type":"message","id":"c3915df7","parentId":"ee73daae","timestamp":"2026-01-01T00:00:06.406Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to run a command that will wait for 30 seconds, and then after it completes, respond with a specific message. But they also say \"Do not answer until it completes\" - but this command will run for 30 seconds and then naturally finish. However, the response they want is \"interrupt fixture should not finish naturally\" which contradicts the fact that it did finish naturally.\n\nWait, let me re-read. They want me to run the command and not answer until it completes. The command is a node script that logs \"interrupt fixture tool started\" and then waits 30 seconds. If I wait for it to complete, it will finish naturally after 30 seconds. But the response they want is \"interrupt fixture should not finish naturally\" - implying that the fixture should have been interrupted before finishing naturally.\n\nThis is a test scenario. Let me just run it and see what happens. Actually, maybe they want me to just run it and respond accordingly. Let me execute the command.\n\nActually, I think this is part of a test where the system might interrupt/kill the process. Let me just run the command with a sufficient timeout.","thinkingSignature":""},{"type":"toolCall","id":"call_8ac3cb4177994abdb9aa1c57","name":"bash","arguments":{"command":"node -e \"console.log('interrupt fixture tool started'); setTimeout(() => {}, 30000)\"","timeout":35}}],"api":"openai-completions","provider":"openrouter","model":"deepseek/deepseek-v4-flash","usage":{"input":77,"output":317,"cacheRead":1792,"cacheWrite":0,"reasoning":284,"totalTokens":2186,"cost":{"input":0.00000322322,"output":0.000026539239999999998,"cacheRead":0.000015002623999999998,"cacheWrite":0,"total":0.000044765084}},"stopReason":"toolUse","timestamp":1767225601972,"responseId":"gen-1790898593-E5hfYfRp6aKBI57pUKss","rawStopReason":"tool_calls","thinkingLevel":"high"}},{"type":"message","id":"641f3d3f","parentId":"c3915df7","timestamp":"2026-01-01T00:00:06.463Z","message":{"role":"toolResult","toolCallId":"call_8ac3cb4177994abdb9aa1c57","toolName":"bash","content":[{"type":"text","text":"interrupt fixture tool started\n\n\nCommand aborted"}],"details":{},"isError":true,"timestamp":1767225606463}},{"type":"message","id":"e3ab1495","parentId":"641f3d3f","timestamp":"2026-01-01T00:00:06.465Z","message":{"role":"assistant","content":[],"api":"openai-completions","provider":"openrouter","model":"deepseek/deepseek-v4-flash","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"error","errorMessage":"This operation was aborted","timestamp":1767225606465,"thinkingLevel":"high"}}],"leafId":"e3ab1495"}}} {"type":"expect_outbound","label":"process_start@p2","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"switch_session@p2","frame":{"type":"switch_session","sessionPath":"/pi-sessions/session-1.jsonl","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands@p2","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000003","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} -{"type":"emit_inbound","label":"response:get_commands@p2","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000004","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"extension_ui_request@p2","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000005","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:switch_session@p2","frame":{"id":"t3-0","type":"response","command":"switch_session","success":true,"data":{"cancelled":false}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/pi_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/pi_transcript.ndjson index 35e3e1d21e8d9306d98d832b7b07fe7835256691..14e710c1ff9d10c1189dedaa5058a2bf7780907d 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/pi_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/pi_transcript.ndjson @@ -1,10 +1,8 @@ {"type":"transcript_start","provider":"pi","protocol":"pi.rpc-jsonl","version":"1.0.0","scenario":"turn_interrupt_mid_tool","metadata":{"generatedBy":"live-pi-recorder","piVersion":"1.0.0","model":"openrouter/deepseek/deepseek-v4-flash","launchArgs":"--provider openrouter --model deepseek/deepseek-v4-flash --no-extensions --no-skills --no-prompt-templates --no-context-files --approve"}} {"type":"expect_outbound","label":"process_start","frame":{"type":"process_start","args":["--mode","rpc","--provider","openrouter","--model","deepseek/deepseek-v4-flash","--no-extensions","--no-skills","--no-prompt-templates","--no-context-files","--approve","--extension",""]}} {"type":"expect_outbound","label":"get_state","frame":{"type":"get_state","id":"t3-0"}} -{"type":"expect_outbound","label":"get_commands","frame":{"type":"get_commands","id":"t3-1"}} {"type":"emit_inbound","label":"extension_ui_request","frame":{"type":"extension_ui_request","id":"00000000-0000-4000-8000-000000000001","method":"notify","message":"t3-code MCP unavailable: T3_MCP_URL or T3_MCP_BEARER_TOKEN is missing.","notifyType":"warning"}} {"type":"emit_inbound","label":"response:get_state","frame":{"id":"t3-0","type":"response","command":"get_state","success":true,"data":{"model":{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}},"thinkingLevel":"high","isStreaming":false,"isCompacting":false,"steeringMode":"one-at-a-time","followUpMode":"one-at-a-time","sessionFile":"/pi-sessions/session-1.jsonl","sessionId":"00000000-0000-4000-8000-000000000002","autoCompactionEnabled":true,"messageCount":0,"pendingMessageCount":0}}} -{"type":"emit_inbound","label":"response:get_commands","frame":{"id":"t3-1","type":"response","command":"get_commands","success":true,"data":{"commands":[]}}} {"type":"expect_outbound","label":"get_available_models","frame":{"type":"get_available_models","id":"t3-2"}} {"type":"emit_inbound","label":"response:get_available_models","frame":{"id":"t3-2","type":"response","command":"get_available_models","success":true,"data":{"models":[{"type":"chat","id":"deepseek/deepseek-v4-flash","name":"DeepSeek: DeepSeek V4 Flash 0423","api":"openai-completions","baseUrl":"https://openrouter.ai/api/v1","provider":"openrouter","reasoning":true,"thinkingLevelMap":{"off":"none","minimal":null,"low":null,"medium":null,"high":"high","xhigh":"xhigh","max":null},"input":["text"],"cost":{"input":0.04186,"output":0.08372,"cacheRead":0.008372,"cacheWrite":0},"contextWindow":1048576,"maxTokens":131072,"compat":{"supportsDeveloperRole":false,"thinkingFormat":"openrouter","supportsStrictMode":true,"sendSessionAffinityHeaders":true,"requiresReasoningContentOnAssistantMessages":true}}]}}} {"type":"expect_outbound","label":"get_entries","frame":{"type":"get_entries","id":"t3-3"}} diff --git a/packages/provider-pi/src/server/adapter.test.ts b/packages/provider-pi/src/server/adapter.test.ts index ff9486cb854c431fdc53d3ee509c2049f18ff87a..562fb5ca079083434c782f905e12dad02f174ee4 100644 --- a/packages/provider-pi/src/server/adapter.test.ts +++ b/packages/provider-pi/src/server/adapter.test.ts @@ -94,10 +94,6 @@ interface FakePi { readonly allRequests: () => ReadonlyArray; /** Data returned by the next `get_session_stats` acks, consumed in order. */ readonly queueStats: (data: unknown) => void; - /** Data returned by the next `get_commands` acks, consumed in order. */ - readonly queueCommands: (data: unknown) => void; - /** Make the next `get_commands` ack fail. */ - readonly failNextCommands: () => void; /** Close the fake process stdout stream. */ readonly closeStdout: Effect.Effect; readonly lastSpawn: () => { @@ -137,7 +133,6 @@ const makeFakePi: Effect.Effect = Effect.gen(function* () { const messagesQueue: Array = []; const stateQueue: Array> = []; const statsQueue: Array = []; - const commandsQueue: Array<{ readonly success: boolean; readonly data?: unknown }> = []; const allRequests: Array = []; let deferState = false; let deferredStateRequest: PiRpcRecord | undefined; @@ -191,8 +186,6 @@ const makeFakePi: Effect.Effect = Effect.gen(function* () { return { ...base, data: messagesQueue.shift() ?? { messages: [] } }; case "get_session_stats": return { ...base, data: statsQueue.shift() ?? {} }; - case "get_commands": - return { ...base, ...(commandsQueue.shift() ?? { data: { commands: [] } }) }; case "fork": return { ...base, data: { text: "Hello pi", cancelled: false } }; default: @@ -305,8 +298,6 @@ const makeFakePi: Effect.Effect = Effect.gen(function* () { }, queueState: (data) => stateQueue.push(data), queueStats: (data) => statsQueue.push(data), - queueCommands: (data) => commandsQueue.push({ success: true, data }), - failNextCommands: () => commandsQueue.push({ success: false }), closeStdout: Queue.end(stdout), lastSpawn: () => lastSpawn, } satisfies FakePi; @@ -1697,22 +1688,9 @@ describe("PiAdapterV2", () => { }).pipe(Effect.scoped, Effect.provide(layerTest)), ); - it.effect("expands a selected $ skill through Pi's native skill command", () => + it.effect("preserves selected skill references for the Pi extension to expand", () => Effect.gen(function* () { const fake = yield* makeFakePi; - fake.queueCommands({ - commands: [ - { - name: "skill:repo-review", - description: "Review this repository.", - source: "skill", - sourceInfo: { - path: "/workspace/.agents/skills/repo-review/SKILL.md", - scope: "project", - }, - }, - ], - }); const { runtime } = yield* openRuntime(fake); const providerThread = yield* runtime.ensureThread({ threadId: THREAD_ID, @@ -1728,43 +1706,7 @@ describe("PiAdapterV2", () => { "Review this change please $repo-review", ); const prompt = yield* fake.takeRequest("prompt"); - assert.equal(prompt["message"], "/skill:repo-review Review this change please"); - }).pipe(Effect.scoped, Effect.provide(layerTest)), - ); - - it.effect("expands every selected $ skill through Pi native skill commands", () => - Effect.gen(function* () { - const fake = yield* makeFakePi; - fake.queueCommands({ - commands: [ - { - name: "skill:repo-review", - source: "skill", - sourceInfo: { - path: "/workspace/.agents/skills/repo-review/SKILL.md", - scope: "project", - }, - }, - { - name: "skill:deploy", - source: "skill", - sourceInfo: { - path: "/workspace/.agents/skills/deploy/SKILL.md", - scope: "project", - }, - }, - ], - }); - const { runtime } = yield* openRuntime(fake); - const providerThread = yield* runtime.ensureThread({ - threadId: THREAD_ID, - modelSelection: modelSelection("default"), - runtimePolicy, - }); - - yield* startTurn(runtime, providerThread, "default", [], "use $repo-review and $deploy"); - const prompt = yield* fake.takeRequest("prompt"); - assert.equal(prompt["message"], "/skill:repo-review /skill:deploy use and"); + assert.equal(prompt["message"], "Review this change please $repo-review"); }).pipe(Effect.scoped, Effect.provide(layerTest)), ); diff --git a/packages/provider-pi/src/server/adapter.ts b/packages/provider-pi/src/server/adapter.ts index f27fead5060562ad198adb0e6e04daa51891b00f..d8dedd71d0b62d9ec760af1f741ecd7bee4b782e 100644 --- a/packages/provider-pi/src/server/adapter.ts +++ b/packages/provider-pi/src/server/adapter.ts @@ -63,12 +63,7 @@ import { ChildProcessSpawner } from "effect/process"; import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import { mcpToolPresentation } from "@t3tools/provider-core/server/mcpToolPresentation"; import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession"; -import { - expandPiSkillReference, - parsePiCompactCommand, - parsePiDiscoveredCommands, - type PiCompactCommand, -} from "./commands.ts"; +import { parsePiCompactCommand, type PiCompactCommand } from "./commands.ts"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; @@ -114,7 +109,6 @@ const STREAM_FLUSH_MS = 50; const PI_REQUEST_TIMEOUT_MS = 15_000; // Session lifecycle hooks reload extensions, MCP servers and language servers. const PI_SESSION_TIMEOUT_MS = 60_000; -const PI_SKILL_DISCOVERY_TIMEOUT_MS = 4_000; const PI_UNSOLICITED_ACTIVITY_ERROR = "Pi started agent work outside an active T3 turn. The session was stopped to prevent invisible tool execution."; const SETTLE_PROBE_MAX_ATTEMPTS = 3; @@ -458,15 +452,6 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* ( }), ), ); - const discoverSkillNames = connection - .request({ type: "get_commands" }, PI_SKILL_DISCOVERY_TIMEOUT_MS) - .pipe( - Effect.map( - (data) => new Set(parsePiDiscoveredCommands(data).skills.map((skill) => skill.name)), - ), - ); - let skillNames: Set | null = null; - const now = yield* DateTime.now; let sessionEntity: OrchestrationV2ProviderSession = { id: input.providerSessionId, @@ -2117,16 +2102,6 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* ( Effect.forkIn(scope), ); - // Discovery can invoke extension code and therefore raise a blocking - // UI request. Start it only after the event pump exists, and never hold - // session opening on it; startup requests are persisted at session - // scope and can be answered before a turn begins. - yield* discoverSkillNames.pipe( - Effect.tap((discovered) => Effect.sync(() => (skillNames = discovered))), - Effect.ignore, - Effect.forkIn(scope), - ); - // ── session runtime ─────────────────────────────────── const registerThread = Effect.fnUntraced(function* ( @@ -2329,15 +2304,6 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* ( text: string, attachments: ReadonlyArray, ) { - // Provider discovery and the live session are separate Pi processes. - // Retry a failed session-local lookup once at first use so a transient - // startup failure cannot leave a visible $ skill inert for this session. - if (skillNames === null && text.includes("$")) { - skillNames = yield* discoverSkillNames.pipe( - Effect.orElseSucceed(() => new Set()), - ); - } - const expandedText = skillNames === null ? text : expandPiSkillReference(text, skillNames); const images: Array<{ type: "image"; data: string; mimeType: string }> = []; const extraLines: Array = []; for (const attachment of attachments) { @@ -2354,8 +2320,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* ( extraLines.push(`[Attachment saved at ${path}]`); } } - const message = - extraLines.length === 0 ? expandedText : `${expandedText}\n\n${extraLines.join("\n")}`; + const message = extraLines.length === 0 ? text : `${text}\n\n${extraLines.join("\n")}`; return { message, images }; }); diff --git a/packages/provider-pi/src/server/commands.test.ts b/packages/provider-pi/src/server/commands.test.ts index 09f39130dd06029bd015d9700acb7d3f1033e2da..ef7b247ee5880febe388ba519260635bb0b0f2af 100644 --- a/packages/provider-pi/src/server/commands.test.ts +++ b/packages/provider-pi/src/server/commands.test.ts @@ -1,7 +1,6 @@ import { expect, it } from "@effect/vitest"; import { - expandPiSkillReference, parsePiCompactCommand, parsePiDiscoveredCommands, PI_COMPACT_SLASH_COMMAND, @@ -109,19 +108,3 @@ it("prepends the builtin compact command without duplicating a discovered one", ]), ).toEqual([PI_COMPACT_SLASH_COMMAND, { name: "hello" }]); }); - -it("leaves unrelated dollar-prefixed text unchanged", () => { - expect(expandPiSkillReference("Explain $HOME", new Set(["global-review"]))).toBe("Explain $HOME"); -}); - -it("hoists every known $ skill and keeps the rest of the prompt", () => { - expect(expandPiSkillReference("use $alpha then $beta please", new Set(["alpha", "beta"]))).toBe( - "/skill:alpha /skill:beta use then please", - ); -}); - -it("preserves code indentation and line breaks when expanding a skill", () => { - expect(expandPiSkillReference("$review\n```ts\n const x = 1;\n```", new Set(["review"]))).toBe( - "/skill:review ```ts\n const x = 1;\n```", - ); -}); diff --git a/packages/provider-pi/src/server/commands.ts b/packages/provider-pi/src/server/commands.ts index 3e02b7718a4527eaa62a250a249b6cb4019c71a7..07b992dd7c27182775ac08aa9bda33c124d4dbdd 100644 --- a/packages/provider-pi/src/server/commands.ts +++ b/packages/provider-pi/src/server/commands.ts @@ -91,41 +91,6 @@ export function parsePiDiscoveredCommands(data: unknown): PiDiscoveredCommands { return { slashCommands, skills }; } -/** - * Pi expands skills only through leading `/skill:name` commands. T3 stores - * skill chips as `$name`, so hoist every known `$skill` to that native - * command position while preserving the rest of the user's prompt. - */ -export function expandPiSkillReference(text: string, skillNames: ReadonlySet): string { - const references = /(^|\s)\$([^\s]+)(?=\s|$)/g; - const found: Array<{ name: string; start: number; end: number }> = []; - for (const match of text.matchAll(references)) { - const name = match[2]; - if (name === undefined || !skillNames.has(name) || match.index === undefined) continue; - const tokenStart = match.index + (match[1]?.length ?? 0); - found.push({ name, start: tokenStart, end: tokenStart + name.length + 1 }); - } - if (found.length === 0) return text; - - const orderedNames: string[] = []; - const seen = new Set(); - for (const token of found) { - if (seen.has(token.name)) continue; - seen.add(token.name); - orderedNames.push(token.name); - } - - let body = text; - for (let index = found.length - 1; index >= 0; index -= 1) { - const token = found[index]; - if (token === undefined) continue; - body = `${body.slice(0, token.start)}${body.slice(token.end)}`; - } - body = body.trim(); - const prefix = orderedNames.map((name) => `/skill:${name}`).join(" "); - return body.length === 0 ? prefix : `${prefix} ${body}`; -} - function recordField(input: unknown, key: string): unknown { return Predicate.isObject(input) ? input[key] : undefined; } diff --git a/packages/provider-pi/src/server/mcpExtensionSource.test.ts b/packages/provider-pi/src/server/mcpExtensionSource.test.ts index 4d4aef33d8fd4def08276c5dee682f1218fe687f..25b040c2c002ed99eb2fbe238c43b711e1dc4a4c 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.test.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.test.ts @@ -1,3 +1,7 @@ +// @effect-diagnostics nodeBuiltinImport:off -- Pi extensions run outside Effect; these tests exercise their native filesystem boundary. +import * as NodeFSP from "node:fs/promises"; +import * as NodePath from "node:path"; +import * as NodeOS from "node:os"; import * as NodeModule from "node:module"; import * as NodeVM from "node:vm"; import { assert, describe, it } from "@effect/vitest"; @@ -9,6 +13,40 @@ type RequestHook = ( ctx: { model: { provider: string } }, ) => Record | undefined; +interface SkillCommand { + readonly name: string; + readonly source: string; + readonly sourceInfo: { readonly path: string }; +} + +type InputHook = ( + event: { text: string; images?: ReadonlyArray }, + ctx: { ui: { notify: (message: string, level: string) => void } }, +) => Promise<{ action: string; text: string; images?: ReadonlyArray } | undefined>; + +async function loadHooks(commands: ReadonlyArray = []) { + const handlers = new Map(); + // Execute the shipped extension with MCP disabled; this path needs no Typebox. + const source = NodeModule.stripTypeScriptTypes( + PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace( + "export default async function", + "async function", + ), + ); + await NodeVM.runInNewContext(`${source}\nt3McpExtension(pi)`, { + process: { env: {} }, + NodeFSP, + NodePath, + // Frontmatter parsing belongs to Pi. These fixtures contain only skill bodies. + stripFrontmatter: (content: string) => content, + pi: { + on: (name: string, handler: unknown) => handlers.set(name, handler), + getCommands: () => commands, + }, + }); + return handlers; +} + interface RegisteredTool { readonly name: string; readonly description: string; @@ -55,7 +93,7 @@ async function loadMcpBridge( { name: "preview_snapshot", description: "Inspect the collaborative browser." }, ].map((tool) => ({ ...tool, inputSchema: { type: "object", properties: {} } })); const source = NodeModule.stripTypeScriptTypes( - PI_T3_MCP_EXTENSION_SOURCE.replace('import { Type } from "typebox";', "").replace( + PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace( "export default async function", "async function", ), @@ -271,7 +309,7 @@ describe("Pi tool discovery permissions", () => { let toolCall: ToolCallHook | undefined; let searchPath = "builtin:tool-search"; const source = NodeModule.stripTypeScriptTypes( - PI_T3_MCP_EXTENSION_SOURCE.replace('import { Type } from "typebox";', "").replace( + PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace( "export default async function", "async function", ), @@ -315,7 +353,7 @@ async function loadRequestHook(): Promise { const handlers = new Map(); // Execute the shipped extension with MCP disabled; this path needs no Typebox. const source = NodeModule.stripTypeScriptTypes( - PI_T3_MCP_EXTENSION_SOURCE.replace('import { Type } from "typebox";', "").replace( + PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace( "export default async function", "async function", ), @@ -359,3 +397,108 @@ describe("Pi upstream output-budget workaround", () => { ); }); }); + +async function loadInputHook(commands: ReadonlyArray) { + const handlers = await loadHooks(commands); + const hook = handlers.get("input"); + assert.isDefined(hook); + return hook as InputHook; +} + +describe("Pi skill references", () => { + it("loads every selected skill once while preserving inline prose, whitespace, and images", async () => { + const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-")); + try { + const alpha = NodePath.join(directory, "alpha.md"); + const beta = NodePath.join(directory, "beta.md"); + await NodeFSP.writeFile(alpha, "ALPHA_INSTRUCTIONS"); + await NodeFSP.writeFile(beta, "BETA_INSTRUCTIONS"); + const hook = await loadInputHook([ + { name: "skill:alpha", source: "skill", sourceInfo: { path: alpha } }, + { name: "skill:beta", source: "skill", sourceInfo: { path: beta } }, + ]); + const text = + "Please use the $alpha philosophy, then $beta and $alpha\n```ts\n const x = 1;\n```"; + const images = [{ type: "image", data: "fixture" }]; + const result = await hook({ text, images }, { ui: { notify: assert.fail } }); + assert.equal(result?.action, "transform"); + assert.isTrue(result?.text.startsWith(text + "\n\n")); + assert.equal(result?.text.split("ALPHA_INSTRUCTIONS").length, 2); + assert.equal(result?.text.split("BETA_INSTRUCTIONS").length, 2); + assert.include(result?.text ?? "", `References are relative to ${directory}.`); + assert.strictEqual(result?.images, images); + } finally { + await NodeFSP.rm(directory, { recursive: true }); + } + }); + + it("leaves unknown references and native skill commands for Pi", async () => { + const hook = await loadInputHook([ + { name: "skill:alpha", source: "skill", sourceInfo: { path: "/unused" } }, + ]); + // Punctuation-adjacent references are plain text in the composer, not selected chips. + for (const text of [ + "Explain $HOME", + "/skill:alpha use $alpha", + "Hello", + "$missing", + "Use $alpha, then continue", + "Use ($alpha)", + ]) { + assert.isUndefined(await hook({ text }, { ui: { notify: assert.fail } })); + } + }); + + it("loads additional chips without duplicating a leading native skill", async () => { + const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-")); + try { + const path = NodePath.join(directory, "beta.md"); + await NodeFSP.writeFile(path, "BETA_INSTRUCTIONS"); + const hook = await loadInputHook([ + { name: "skill:alpha", source: "skill", sourceInfo: { path: "/unused" } }, + { name: "skill:beta", source: "skill", sourceInfo: { path } }, + ]); + const text = "/skill:alpha use $alpha and $beta"; + const result = await hook({ text }, { ui: { notify: assert.fail } }); + assert.isTrue(result?.text.startsWith(text + "\n\n")); + assert.notInclude(result?.text ?? "", ' { + const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-")); + try { + const path = NodePath.join(directory, "readable.md"); + await NodeFSP.writeFile(path, "READABLE_INSTRUCTIONS"); + const hook = await loadInputHook([ + { + name: "skill:missing", + source: "skill", + sourceInfo: { path: NodePath.join(directory, "missing.md") }, + }, + { name: "skill:readable", source: "skill", sourceInfo: { path } }, + ]); + const notices: string[] = []; + const text = "Use $missing and $readable"; + const result = await hook( + { text }, + { + ui: { + notify: (message) => { + notices.push(message); + }, + }, + }, + ); + assert.isTrue(result?.text.startsWith(text + "\n\n")); + assert.include(result?.text ?? "", "READABLE_INSTRUCTIONS"); + assert.equal(notices.length, 1); + assert.include(notices[0] ?? "", "Could not load skill missing"); + } finally { + await NodeFSP.rm(directory, { recursive: true }); + } + }); +}); diff --git a/packages/provider-pi/src/server/mcpExtensionSource.ts b/packages/provider-pi/src/server/mcpExtensionSource.ts index e11e953f8433962cf3057f9025463f2ea4f09120..8279a515ed65fc9697c39f3184e2b3b78102cc20 100644 --- a/packages/provider-pi/src/server/mcpExtensionSource.ts +++ b/packages/provider-pi/src/server/mcpExtensionSource.ts @@ -23,7 +23,9 @@ export const T3_PI_RUNTIME_MODE_ENV = "T3_PI_RUNTIME_MODE"; export const PI_FILE_CHANGE_TOOLS = ["edit", "write"] as const; export const PI_T3_MCP_EXTENSION_SOURCE = `\ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { stripFrontmatter, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import * as NodeFSP from "node:fs/promises"; +import * as NodePath from "node:path"; import { Type } from "typebox"; const URL_ENV = ${JSON.stringify(T3_MCP_URL_ENV)}; @@ -210,6 +212,38 @@ function createMcpClient(endpoint: string, token: string) { } export default async function t3McpExtension(pi: ExtensionAPI) { + // Keep skill chips in the request. Pi expands only one leading /skill: + // command, so use its own frontmatter loader for every selected chip here. + pi.on("input", async (event, ctx) => { + if (!event.text.includes("$")) return; + const commands = pi.getCommands().filter((command) => command.source === "skill"); + const nativeSkill = /^\\/skill:([^ ]+)(?: |$)/.exec(event.text)?.[1]; + const seen = new Set(nativeSkill === undefined ? [] : [nativeSkill]); + const blocks: string[] = []; + for (const match of event.text.matchAll(/(^|\\s)\\$([^\\s]+)(?=\\s|$)/g)) { + const name = match[2]; + if (name === undefined || seen.has(name)) continue; + const command = commands.find((candidate) => candidate.name === "skill:" + name); + if (command === undefined) continue; + seen.add(name); + const path = command.sourceInfo.path; + try { + const body = stripFrontmatter(await NodeFSP.readFile(path, "utf8")).trim(); + blocks.push( + \`\\nReferences are relative to \${NodePath.dirname(path)}.\\n\\n\${body}\\n\`, + ); + } catch { + ctx.ui.notify(\`Could not load skill \${name} from \${path}.\`, "error"); + } + } + if (blocks.length === 0) return; + return { + action: "transform", + text: event.text + "\\n\\n" + blocks.join("\\n\\n"), + images: event.images, + }; + }); + // Workaround for an upstream Pi context-budgeting bug: pi-ai reuses the // previous response's usage even when a fork's instructions/tools differ, // then reserves almost all remaining context for output. OpenRouter can -- 2.54.0