pi-amplike 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,8 +5,10 @@
5
5
  ## Features
6
6
 
7
7
  ### Session Management
8
- - **`/handoff <goal>`** - Create a new focused session based on the current one with context compacted based on a given goal
9
- - **`session_query`** tool - The agent in the handed off session automatically gets the ability to query the parent session for context, decisions, or code changes
8
+ - **Handoff** - Create a new focused session with AI-generated context transfer:
9
+ - **`/handoff <goal>`** command - Manually create a handoff session
10
+ - **`handoff` tool** - The agent can invoke this when you explicitly request a handoff
11
+ - **`session_query`** tool - The agent in handed-off sessions automatically gets the ability to query the parent session for context, decisions, or code changes
10
12
  - Use `/resume` to switch between and navigate handed-off sessions
11
13
 
12
14
  ### Web Access
@@ -47,19 +49,27 @@ Get an API key at [jina.ai](https://jina.ai/). Even if you charge only the minim
47
49
 
48
50
  ### Session Handoff
49
51
 
50
- When your conversation gets long or you want to branch off to a focused task:
52
+ When your conversation gets long or you want to branch off to a focused task, you can use handoff in two ways:
51
53
 
54
+ **Manual handoff via command:**
52
55
  ```
53
56
  /handoff now implement this for teams as well
54
57
  /handoff execute phase one of the plan
55
58
  /handoff check other places that need this fix
56
59
  ```
57
60
 
58
- This creates a new session with:
59
- - Summarized context from the current conversation
60
- - List of relevant files
61
+ **Agent-invoked handoff:**
62
+ The agent can also initiate a handoff when you explicitly ask for it:
63
+ ```
64
+ "Please hand this off to a new session to implement the fix"
65
+ "Create a handoff session to execute phase one"
66
+ ```
67
+
68
+ Both methods create a new session with:
69
+ - AI-generated summary of relevant context from the current conversation
70
+ - List of relevant files that were discussed or modified
61
71
  - Clear task description based on your goal
62
- - Reference to parent session (for later querying)
72
+ - Reference to parent session (accessible via `session_query` tool)
63
73
 
64
74
  ### Session Navigation
65
75
 
@@ -90,8 +100,8 @@ session_query("/path/to/session.jsonl", "What approach was chosen?")
90
100
 
91
101
  | Component | Type | Description |
92
102
  |-----------|------|-------------|
93
- | [handoff](extensions/handoff.ts) | Extension | `/handoff` command for context transfer |
94
- | [session-query](extensions/session-query.ts) | Extension | `session_query` tool for the model |
103
+ | [handoff](extensions/handoff.ts) | Extension | `/handoff` command + `handoff` tool for AI-powered context transfer |
104
+ | [session-query](extensions/session-query.ts) | Extension | `session_query` tool for querying parent sessions |
95
105
  | [session-query](skills/session-query/) | Skill | Instructions for using the session_query tool |
96
106
  | [web-search](skills/web-search/) | Skill | Web search via Jina API |
97
107
  | [visit-webpage](skills/visit-webpage/) | Skill | Webpage content extraction |
@@ -4,6 +4,10 @@
4
4
  * Instead of compacting (which is lossy), handoff extracts what matters
5
5
  * for your next task and creates a new session with a generated prompt.
6
6
  *
7
+ * Provides both:
8
+ * - /handoff command: user types `/handoff <goal>`
9
+ * - handoff tool: agent can call when user explicitly requests a handoff
10
+ *
7
11
  * Usage:
8
12
  * /handoff now implement this for teams as well
9
13
  * /handoff execute phase one of the plan
@@ -13,8 +17,9 @@
13
17
  */
14
18
 
15
19
  import { complete, type Message } from "@mariozechner/pi-ai";
16
- import type { ExtensionAPI, SessionEntry } from "@mariozechner/pi-coding-agent";
20
+ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@mariozechner/pi-coding-agent";
17
21
  import { BorderedLoader, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
22
+ import { Type } from "@sinclair/typebox";
18
23
 
19
24
  const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
20
25
 
@@ -38,114 +43,140 @@ Files involved:
38
43
  ## Task
39
44
  [Clear description of what to do next based on user's goal]`;
40
45
 
46
+ /**
47
+ * Core handoff logic. Returns an error string on failure, or undefined on success.
48
+ */
49
+ async function performHandoff(pi: ExtensionAPI, ctx: ExtensionContext, goal: string, fromTool = false): Promise<string | undefined> {
50
+ if (!ctx.hasUI) {
51
+ return "Handoff requires interactive mode.";
52
+ }
53
+
54
+ if (!ctx.model) {
55
+ return "No model selected.";
56
+ }
57
+
58
+ const branch = ctx.sessionManager.getBranch();
59
+ const messages = branch
60
+ .filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
61
+ .map((entry) => entry.message);
62
+
63
+ if (messages.length === 0) {
64
+ return "No conversation to hand off.";
65
+ }
66
+
67
+ const llmMessages = convertToLlm(messages);
68
+ const conversationText = serializeConversation(llmMessages);
69
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
70
+
71
+ // Generate the handoff prompt with loader UI
72
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
73
+ const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
74
+ loader.onAbort = () => done(null);
75
+
76
+ const doGenerate = async () => {
77
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
78
+
79
+ const userMessage: Message = {
80
+ role: "user",
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`,
85
+ },
86
+ ],
87
+ timestamp: Date.now(),
88
+ };
89
+
90
+ const response = await complete(
91
+ ctx.model!,
92
+ { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
93
+ { apiKey, signal: loader.signal },
94
+ );
95
+
96
+ if (response.stopReason === "aborted") {
97
+ return null;
98
+ }
99
+
100
+ return response.content
101
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
102
+ .map((c) => c.text)
103
+ .join("\n");
104
+ };
105
+
106
+ doGenerate()
107
+ .then(done)
108
+ .catch((err) => {
109
+ console.error("Handoff generation failed:", err);
110
+ done(null);
111
+ });
112
+
113
+ return loader;
114
+ });
115
+
116
+ if (result === null) {
117
+ return "Handoff cancelled.";
118
+ }
119
+
120
+ // Build the final prompt with user's goal first for easy identification
121
+ let finalPrompt = result;
122
+ if (currentSessionFile) {
123
+ finalPrompt = `${goal}\n\n/skill:session-query\n\n**Parent session:** \`${currentSessionFile}\`\n\n${result}`;
124
+ } else {
125
+ finalPrompt = `${goal}\n\n${result}`;
126
+ }
127
+
128
+ // Create new session and send the prompt.
129
+ // When called from a tool, we must defer the session switch until after
130
+ // the current turn completes (tool_result is recorded), otherwise the
131
+ // new session gets a tool_result without a corresponding tool_use block.
132
+ const sm = ctx.sessionManager as any;
133
+ const doSwitch = () => {
134
+ sm.newSession({ parentSession: currentSessionFile });
135
+ pi.sendUserMessage(finalPrompt, { deliverAs: "followUp" });
136
+ };
137
+
138
+ if (fromTool) {
139
+ // Defer to next tick so the tool_result is recorded in the OLD session first
140
+ setTimeout(doSwitch, 0);
141
+ } else {
142
+ doSwitch();
143
+ }
144
+ return undefined;
145
+ }
146
+
41
147
  export default function (pi: ExtensionAPI) {
148
+ // /handoff command
42
149
  pi.registerCommand("handoff", {
43
150
  description: "Transfer context to a new focused session",
44
151
  handler: async (args, ctx) => {
45
- if (!ctx.hasUI) {
46
- ctx.ui.notify("handoff requires interactive mode", "error");
47
- return;
48
- }
49
-
50
- if (!ctx.model) {
51
- ctx.ui.notify("No model selected", "error");
52
- return;
53
- }
54
-
55
152
  const goal = args.trim();
56
153
  if (!goal) {
57
154
  ctx.ui.notify("Usage: /handoff <goal for new thread>", "error");
58
155
  return;
59
156
  }
60
157
 
61
- // Gather conversation context from current branch
62
- const branch = ctx.sessionManager.getBranch();
63
- const messages = branch
64
- .filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
65
- .map((entry) => entry.message);
66
-
67
- if (messages.length === 0) {
68
- ctx.ui.notify("No conversation to hand off", "error");
69
- return;
70
- }
71
-
72
- // Convert to LLM format and serialize
73
- const llmMessages = convertToLlm(messages);
74
- const conversationText = serializeConversation(llmMessages);
75
- const currentSessionFile = ctx.sessionManager.getSessionFile();
76
-
77
- // Generate the handoff prompt with loader UI
78
- const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
79
- const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
80
- loader.onAbort = () => done(null);
81
-
82
- const doGenerate = async () => {
83
- const apiKey = await ctx.modelRegistry.getApiKey(ctx.model!);
84
-
85
- const userMessage: Message = {
86
- role: "user",
87
- content: [
88
- {
89
- type: "text",
90
- text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`,
91
- },
92
- ],
93
- timestamp: Date.now(),
94
- };
95
-
96
- const response = await complete(
97
- ctx.model!,
98
- { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
99
- { apiKey, signal: loader.signal },
100
- );
101
-
102
- if (response.stopReason === "aborted") {
103
- return null;
104
- }
105
-
106
- return response.content
107
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
108
- .map((c) => c.text)
109
- .join("\n");
110
- };
111
-
112
- doGenerate()
113
- .then(done)
114
- .catch((err) => {
115
- console.error("Handoff generation failed:", err);
116
- done(null);
117
- });
118
-
119
- return loader;
120
- });
121
-
122
- if (result === null) {
123
- ctx.ui.notify("Cancelled", "info");
124
- return;
125
- }
126
-
127
- // Create new session with parent tracking
128
- const newSessionResult = await ctx.newSession({
129
- parentSession: currentSessionFile,
130
- });
131
-
132
- if (newSessionResult.cancelled) {
133
- ctx.ui.notify("New session cancelled", "info");
134
- return;
135
- }
136
-
137
- // Build the final prompt with user's goal first for easy identification
138
- // Format: goal (first line for session preview) → skill → parent ref → context
139
- let finalPrompt = result;
140
- if (currentSessionFile) {
141
- finalPrompt = `${goal}\n\n/skill:session-query\n\n**Parent session:** \`${currentSessionFile}\`\n\n${result}`;
142
- } else {
143
- // Even without parent session, put goal first
144
- finalPrompt = `${goal}\n\n${result}`;
158
+ const error = await performHandoff(pi, ctx, goal);
159
+ if (error) {
160
+ ctx.ui.notify(error, "error");
145
161
  }
162
+ },
163
+ });
146
164
 
147
- // Immediately submit the handoff prompt to start the agent
148
- pi.sendUserMessage(finalPrompt);
165
+ // handoff tool (agent-callable)
166
+ pi.registerTool({
167
+ name: "handoff",
168
+ label: "Handoff",
169
+ description:
170
+ "Transfer context to a new focused session. ONLY use this when the user explicitly asks for a handoff. Provide a goal describing what the new session should focus on.",
171
+ parameters: Type.Object({
172
+ goal: Type.String({ description: "The goal/task for the new session" }),
173
+ }),
174
+
175
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
176
+ const error = await performHandoff(pi, ctx, params.goal, true);
177
+ return {
178
+ content: [{ type: "text", text: error ?? "Handoff complete. New session started with the generated prompt." }],
179
+ };
149
180
  },
150
181
  });
151
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-amplike",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Pi skills and extensions that provide Amp Code-like workflows (handoff, session query, web search, visit webpage).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/pasky/pi-amplike",