pi-amplike 0.1.1 → 0.1.3
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 +19 -9
- package/extensions/handoff.ts +125 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
### Session Management
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 (
|
|
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
|
|
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 |
|
package/extensions/handoff.ts
CHANGED
|
@@ -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,137 @@ 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
|
+
// Create new session directly via sessionManager.
|
|
121
|
+
// ctx.newSession() is only available on ExtensionCommandContext (commands),
|
|
122
|
+
// but sessionManager.newSession() works at runtime from any context.
|
|
123
|
+
const sm = ctx.sessionManager as any;
|
|
124
|
+
sm.newSession({ parentSession: currentSessionFile });
|
|
125
|
+
|
|
126
|
+
// Build the final prompt with user's goal first for easy identification
|
|
127
|
+
let finalPrompt = result;
|
|
128
|
+
if (currentSessionFile) {
|
|
129
|
+
finalPrompt = `${goal}\n\n/skill:session-query\n\n**Parent session:** \`${currentSessionFile}\`\n\n${result}`;
|
|
130
|
+
} else {
|
|
131
|
+
finalPrompt = `${goal}\n\n${result}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Submit the handoff prompt. From a tool, the agent is still streaming,
|
|
135
|
+
// so use followUp to queue it after the current turn completes.
|
|
136
|
+
if (fromTool) {
|
|
137
|
+
pi.sendUserMessage(finalPrompt, { deliverAs: "followUp" });
|
|
138
|
+
} else {
|
|
139
|
+
pi.sendUserMessage(finalPrompt);
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
41
144
|
export default function (pi: ExtensionAPI) {
|
|
145
|
+
// /handoff command
|
|
42
146
|
pi.registerCommand("handoff", {
|
|
43
147
|
description: "Transfer context to a new focused session",
|
|
44
148
|
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
149
|
const goal = args.trim();
|
|
56
150
|
if (!goal) {
|
|
57
151
|
ctx.ui.notify("Usage: /handoff <goal for new thread>", "error");
|
|
58
152
|
return;
|
|
59
153
|
}
|
|
60
154
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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}`;
|
|
155
|
+
const error = await performHandoff(pi, ctx, goal);
|
|
156
|
+
if (error) {
|
|
157
|
+
ctx.ui.notify(error, "error");
|
|
145
158
|
}
|
|
159
|
+
},
|
|
160
|
+
});
|
|
146
161
|
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
// handoff tool (agent-callable)
|
|
163
|
+
pi.registerTool({
|
|
164
|
+
name: "handoff",
|
|
165
|
+
label: "Handoff",
|
|
166
|
+
description:
|
|
167
|
+
"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.",
|
|
168
|
+
parameters: Type.Object({
|
|
169
|
+
goal: Type.String({ description: "The goal/task for the new session" }),
|
|
170
|
+
}),
|
|
171
|
+
|
|
172
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
173
|
+
const error = await performHandoff(pi, ctx, params.goal, true);
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: error ?? "Handoff complete. New session started with the generated prompt." }],
|
|
176
|
+
};
|
|
149
177
|
},
|
|
150
178
|
});
|
|
151
179
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-amplike",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|