@zetaloop/chappie 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/docs/tools.md +51 -3
- package/package.json +1 -1
- package/src/activity.ts +21 -0
- package/src/broker.ts +459 -116
- package/src/config.ts +1 -1
- package/src/delivery.ts +3 -2
- package/src/history.ts +123 -0
- package/src/instructions.md +5 -1
- package/src/ipc.ts +14 -1
- package/src/provider.ts +2 -1
- package/src/server.ts +140 -34
- package/src/session.ts +49 -7
- package/src/state.ts +13 -32
- package/src/tools.ts +10 -1
package/src/config.ts
CHANGED
|
@@ -5,8 +5,8 @@ import * as z from "zod";
|
|
|
5
5
|
const configSchema = z.object({
|
|
6
6
|
ask: z.boolean().optional(),
|
|
7
7
|
connect: z.string().min(1).optional(),
|
|
8
|
-
latestWorkflow: z.boolean().optional(),
|
|
9
8
|
listen: z.union([z.boolean(), z.number().int().min(1).max(65535)]).optional(),
|
|
9
|
+
sync: z.boolean().optional(),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
export async function readConfig(agentDir: string) {
|
package/src/delivery.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ToolResultMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { Source } from "./activity.ts";
|
|
2
3
|
import {
|
|
3
4
|
contentWithImageReferences,
|
|
4
5
|
resourceDescriptors,
|
|
5
6
|
} from "./resources.ts";
|
|
6
7
|
|
|
7
|
-
export interface DeliveryRecord {
|
|
8
|
+
export interface DeliveryRecord extends Source {
|
|
8
9
|
id: string;
|
|
9
|
-
chatId: string;
|
|
10
10
|
sessionId: string;
|
|
11
11
|
cwd: string;
|
|
12
12
|
toolResults: ToolResultMessage[];
|
|
@@ -43,6 +43,7 @@ export function deliveryContent(deliveries: DeliveryRecord[]) {
|
|
|
43
43
|
type: "text" as const,
|
|
44
44
|
text: JSON.stringify({
|
|
45
45
|
deferredResult: delivery.id,
|
|
46
|
+
requestId: delivery.requestId,
|
|
46
47
|
sessionId: delivery.sessionId,
|
|
47
48
|
cwd: delivery.cwd,
|
|
48
49
|
error: delivery.error,
|
package/src/history.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import type { Source } from "./activity.ts";
|
|
4
|
+
import { toolResultsContent } from "./delivery.ts";
|
|
5
|
+
import type { ProviderOutput } from "./provider.ts";
|
|
6
|
+
import { contentWithImageReferences, rememberImages } from "./resources.ts";
|
|
7
|
+
|
|
8
|
+
export const historyInput = z.object({
|
|
9
|
+
limit: z
|
|
10
|
+
.number()
|
|
11
|
+
.int()
|
|
12
|
+
.min(1)
|
|
13
|
+
.default(20)
|
|
14
|
+
.describe("Maximum number of history entries"),
|
|
15
|
+
before: z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Read entries before this entry ID"),
|
|
20
|
+
after: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Read entries after this entry ID, starting with the earliest"),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type HistoryRange = z.infer<typeof historyInput>;
|
|
28
|
+
export type HistoryResult = ReturnType<typeof historyResult>;
|
|
29
|
+
|
|
30
|
+
export const historyInstructions =
|
|
31
|
+
"When resuming work, read recent history to recover progress, then continue from the current request.";
|
|
32
|
+
|
|
33
|
+
export function historyResult(
|
|
34
|
+
branch: SessionEntry[],
|
|
35
|
+
sessionId: string,
|
|
36
|
+
{ limit, before, after }: HistoryRange,
|
|
37
|
+
) {
|
|
38
|
+
const start = after ? entryIndex(branch, after) + 1 : 0;
|
|
39
|
+
const end = before ? entryIndex(branch, before) : branch.length;
|
|
40
|
+
if (start > end) throw new Error("History after must precede before");
|
|
41
|
+
const entries = branch.slice(start, end).filter((entry) => {
|
|
42
|
+
switch (entry.type) {
|
|
43
|
+
case "message":
|
|
44
|
+
return (
|
|
45
|
+
entry.message.role !== "custom" ||
|
|
46
|
+
entry.message.customType !== "chappie.request"
|
|
47
|
+
);
|
|
48
|
+
case "custom_message":
|
|
49
|
+
return entry.customType !== "chappie.request";
|
|
50
|
+
case "custom":
|
|
51
|
+
return entry.customType === "chappie.notice";
|
|
52
|
+
case "compaction":
|
|
53
|
+
case "branch_summary":
|
|
54
|
+
return true;
|
|
55
|
+
default:
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
const selected = after ? entries.slice(0, limit) : entries.slice(-limit);
|
|
60
|
+
const sources = new Map<string, Source>();
|
|
61
|
+
for (const entry of branch) {
|
|
62
|
+
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
63
|
+
continue;
|
|
64
|
+
const message = entry.message as ProviderOutput["message"];
|
|
65
|
+
if (!message.chappie) continue;
|
|
66
|
+
for (const block of message.content) {
|
|
67
|
+
if (block.type === "toolCall") sources.set(block.id, message.chappie);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
count: selected.length,
|
|
72
|
+
hasMore: entries.length > selected.length,
|
|
73
|
+
content: selected.flatMap((entry) =>
|
|
74
|
+
entryContent(entry, sessionId, sources),
|
|
75
|
+
),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function entryIndex(entries: SessionEntry[], id: string): number {
|
|
80
|
+
const index = entries.findIndex((entry) => entry.id === id);
|
|
81
|
+
if (index === -1)
|
|
82
|
+
throw new Error(`History entry ${id} is not on the current branch`);
|
|
83
|
+
return index;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function entryContent(
|
|
87
|
+
entry: SessionEntry,
|
|
88
|
+
sessionId: string,
|
|
89
|
+
sources: Map<string, Source>,
|
|
90
|
+
): ReturnType<typeof toolResultsContent> {
|
|
91
|
+
const record: Record<string, unknown> = { ...entry };
|
|
92
|
+
const message = entry.type === "message" ? entry.message : entry;
|
|
93
|
+
if (!("content" in message)) {
|
|
94
|
+
return [{ type: "text", text: JSON.stringify(record) }];
|
|
95
|
+
}
|
|
96
|
+
const { content, ...metadata } = message;
|
|
97
|
+
if (entry.type === "message") {
|
|
98
|
+
const source =
|
|
99
|
+
entry.message.role === "toolResult"
|
|
100
|
+
? sources.get(entry.message.toolCallId)
|
|
101
|
+
: undefined;
|
|
102
|
+
record.message = { ...metadata, ...(source ? { chappie: source } : {}) };
|
|
103
|
+
} else delete record.content;
|
|
104
|
+
const header = { type: "text" as const, text: JSON.stringify(record) };
|
|
105
|
+
if (entry.type === "message" && entry.message.role === "toolResult") {
|
|
106
|
+
rememberImages(sessionId, entry.message.content);
|
|
107
|
+
return [header, ...toolResultsContent([entry.message], sessionId)];
|
|
108
|
+
}
|
|
109
|
+
const blocks =
|
|
110
|
+
typeof content === "string"
|
|
111
|
+
? [{ type: "text" as const, text: content }]
|
|
112
|
+
: content;
|
|
113
|
+
return [
|
|
114
|
+
header,
|
|
115
|
+
...blocks.flatMap((block): ReturnType<typeof toolResultsContent> => {
|
|
116
|
+
if (block.type === "text" || block.type === "image") {
|
|
117
|
+
rememberImages(sessionId, [block]);
|
|
118
|
+
return contentWithImageReferences(sessionId, [block]);
|
|
119
|
+
}
|
|
120
|
+
return [{ type: "text", text: JSON.stringify(block) }];
|
|
121
|
+
}),
|
|
122
|
+
];
|
|
123
|
+
}
|
package/src/instructions.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
Chappie connects this ChatGPT conversation to Pi sessions on one or more devices. Resume work with init using the task's Pi sessionId, including in a new chat or branch. When only a project or session name is known, find its device/cwd/name in sessions, then call init. If the target is absent or ambiguous, ask the user to resolve it. Pi sessions appear while the chappie/chatgpt provider is selected.
|
|
2
2
|
|
|
3
|
-
init sets this chat's default Pi session.
|
|
3
|
+
init sets this chat's default Pi session. An execution tool also establishes a default on first use: sessionId selects its target, or omitting it selects the first online session with no saved bindings. These sessions may already contain work. Once a default exists, another tool's sessionId selects only that call's target. Defaults survive broker restarts, and several chats can share one Pi session. When resuming work after explicit or implicit initialization, read recent history to recover progress, then continue from the current request.
|
|
4
|
+
|
|
5
|
+
Use initialization.name, derived from the initialization request suffix, to identify yourself during coordination.
|
|
4
6
|
|
|
5
7
|
The active model is this existing ChatGPT conversation. A Pi tool that starts another chappie/chatgpt agent has no ChatGPT conversation to attach to and will wait indefinitely. Subagents targeting another configured model keep that provider's normal behavior.
|
|
6
8
|
|
|
@@ -13,3 +15,5 @@ Use chat for assistant messages in Pi, including progress, explanations, and res
|
|
|
13
15
|
transfer pairs files from ChatGPT with Pi destination paths in order. Omit files to return resource links for Pi paths or chappie:// image references. Relative paths use the Pi working directory; overwrite: true replaces existing targets. The host may request confirmation when retrieving exported bytes.
|
|
14
16
|
|
|
15
17
|
Tool results identify the executing Pi sessionId and cwd. A shell command can access another directory without changing its Pi session. structuredContent.text includes the complete text, new Pi input, webAnswer, and deferred results; images and resources are native content blocks. Continue from received results rather than repeating work. Host deadlines include queueing and execution; use local persistent processes for longer work.
|
|
18
|
+
|
|
19
|
+
Use history to read the current Pi branch with original entry IDs and timestamps, including chat activity records. Assistant messages and tool results expose their originating chatId and requestId in message.chappie; activity records include the same fields. Parallel executions can share a chatId and workflow ID. Workflow timing helps explain differences in their task goals. History defaults to the last 20 entries; before pages backward and after pages forward. History is a record of past work, separate from new Pi input. It leaves a reading notice and returns its content only to the caller.
|
package/src/ipc.ts
CHANGED
|
@@ -17,7 +17,9 @@ import type {
|
|
|
17
17
|
SlashCommandInfo,
|
|
18
18
|
ToolInfo,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import type { Activity } from "./activity.ts";
|
|
20
21
|
import type { DeliveryRecord } from "./delivery.ts";
|
|
22
|
+
import type { HistoryRange, HistoryResult } from "./history.ts";
|
|
21
23
|
import type { ResourceData } from "./resources.ts";
|
|
22
24
|
|
|
23
25
|
const defaultPort = 24274;
|
|
@@ -57,6 +59,7 @@ export type SessionResult =
|
|
|
57
59
|
toolResults: ToolResultMessage[];
|
|
58
60
|
inputs: SessionInput[];
|
|
59
61
|
}
|
|
62
|
+
| { history: HistoryResult; cwd: string }
|
|
60
63
|
| { resource: ResourceData }
|
|
61
64
|
| { error: string };
|
|
62
65
|
|
|
@@ -69,12 +72,21 @@ export type SessionMessage =
|
|
|
69
72
|
export type BrokerMessage =
|
|
70
73
|
| { type: "synced"; id: number; sessionId: string }
|
|
71
74
|
| { type: "stored"; id: string }
|
|
72
|
-
| { type: "notice"; sessionId: string; message: string }
|
|
75
|
+
| { type: "notice"; sessionId: string; message: string; activity?: Activity }
|
|
73
76
|
| { type: "inspect"; id: number; sessionId: string }
|
|
77
|
+
| {
|
|
78
|
+
type: "history";
|
|
79
|
+
id: number;
|
|
80
|
+
sessionId: string;
|
|
81
|
+
range: HistoryRange;
|
|
82
|
+
chatId: string;
|
|
83
|
+
requestId?: string;
|
|
84
|
+
}
|
|
74
85
|
| {
|
|
75
86
|
type: "chat";
|
|
76
87
|
id: number;
|
|
77
88
|
chatId: string;
|
|
89
|
+
requestId?: string;
|
|
78
90
|
sessionId: string;
|
|
79
91
|
text: string;
|
|
80
92
|
}
|
|
@@ -82,6 +94,7 @@ export type BrokerMessage =
|
|
|
82
94
|
type: "call";
|
|
83
95
|
id: number;
|
|
84
96
|
chatId: string;
|
|
97
|
+
requestId?: string;
|
|
85
98
|
sessionId: string;
|
|
86
99
|
calls: ToolCall[];
|
|
87
100
|
}
|
package/src/provider.ts
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
type StreamOptions,
|
|
10
10
|
type ToolCall,
|
|
11
11
|
} from "@earendil-works/pi-ai";
|
|
12
|
+
import type { Source } from "./activity.ts";
|
|
12
13
|
|
|
13
14
|
export class ProviderOutput {
|
|
14
15
|
readonly stream: AssistantMessageEventStream;
|
|
15
|
-
readonly message: AssistantMessage;
|
|
16
|
+
readonly message: AssistantMessage & { chappie?: Source };
|
|
16
17
|
readonly finished: Promise<void>;
|
|
17
18
|
#resolveFinished: () => void;
|
|
18
19
|
#removeAbort?: () => void;
|
package/src/server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as z from "zod";
|
|
|
4
4
|
import packageJson from "../package.json" with { type: "json" };
|
|
5
5
|
import type { Broker } from "./broker.ts";
|
|
6
6
|
import { deliveryContent } from "./delivery.ts";
|
|
7
|
+
import { historyInput } from "./history.ts";
|
|
7
8
|
import {
|
|
8
9
|
answerContent,
|
|
9
10
|
answerInput,
|
|
@@ -31,6 +32,9 @@ const outputSchema = z.object({
|
|
|
31
32
|
),
|
|
32
33
|
});
|
|
33
34
|
|
|
35
|
+
const syncInstructions =
|
|
36
|
+
"On conflicting activity, start sync and verify with your own initialization code. Keep codes private. Verification returns a new code to the coordinator; ordinary tools and initialization stay locked. Discuss through chat and history, including after failed verification. The coordinator decides who continues, their tasks, and who exits, including retaining only one execution. Acknowledge the decision; if asked to exit, leave a chat handoff and end your response. Only the coordinator releases after decisions and exits are confirmed.";
|
|
37
|
+
|
|
34
38
|
const questionTemplate = "ui://chappie/question.html";
|
|
35
39
|
const questionSchema = outputSchema.extend({ question: questionOutput });
|
|
36
40
|
const toolAnnotations = {
|
|
@@ -54,26 +58,30 @@ export function createServer(broker: Broker): McpServer {
|
|
|
54
58
|
version: packageJson.version,
|
|
55
59
|
},
|
|
56
60
|
{
|
|
57
|
-
instructions:
|
|
58
|
-
|
|
59
|
-
:
|
|
61
|
+
instructions: [
|
|
62
|
+
instructions,
|
|
63
|
+
...(broker.askEnabled ? [questionInstructions] : []),
|
|
64
|
+
...(broker.syncEnabled ? [syncInstructions] : []),
|
|
65
|
+
].join("\n\n"),
|
|
60
66
|
},
|
|
61
67
|
);
|
|
62
68
|
|
|
63
69
|
function handle<Args, Result>(
|
|
64
70
|
callback: (args: Args, context: RequestContext) => Promise<Result>,
|
|
71
|
+
communication = false,
|
|
65
72
|
) {
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
return (args: Args, context: RequestContext): Promise<Result> =>
|
|
74
|
+
broker.run(
|
|
75
|
+
requireChatId(context),
|
|
76
|
+
(args as { sessionId?: string }).sessionId,
|
|
70
77
|
context.mcpReq.signal,
|
|
78
|
+
(signal) =>
|
|
79
|
+
callback(args, {
|
|
80
|
+
...context,
|
|
81
|
+
mcpReq: { ...context.mcpReq, signal },
|
|
82
|
+
}),
|
|
83
|
+
communication,
|
|
71
84
|
);
|
|
72
|
-
return callback(args, {
|
|
73
|
-
...context,
|
|
74
|
-
mcpReq: { ...context.mcpReq, signal },
|
|
75
|
-
});
|
|
76
|
-
};
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
server.registerTool(
|
|
@@ -81,7 +89,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
81
89
|
{
|
|
82
90
|
title: "Connect to Pi",
|
|
83
91
|
description:
|
|
84
|
-
"Select this chat's default Pi session and return its environment and
|
|
92
|
+
"Select this chat's default Pi session and return its environment, tool catalog, and initialization name for coordination. Use the task's sessionId to resume, or find it by cwd/name with sessions. For a task without a specified target, omit sessionId to reuse the default or select the first online, unbound session. Read recent history when resuming work.",
|
|
85
93
|
outputSchema,
|
|
86
94
|
inputSchema: z.object({
|
|
87
95
|
sessionId: z
|
|
@@ -123,7 +131,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
123
131
|
},
|
|
124
132
|
handle(async (args, context) => {
|
|
125
133
|
const chatId = requireChatId(context);
|
|
126
|
-
const { sessionId, cwd, inputs } = await broker.chat(
|
|
134
|
+
const { sessionId, cwd, inputs, initialization } = await broker.chat(
|
|
127
135
|
chatId,
|
|
128
136
|
args.sessionId,
|
|
129
137
|
args.text,
|
|
@@ -133,9 +141,12 @@ export function createServer(broker: Broker): McpServer {
|
|
|
133
141
|
return finishResult(
|
|
134
142
|
broker,
|
|
135
143
|
context,
|
|
136
|
-
textResult(
|
|
144
|
+
textResult(
|
|
145
|
+
{ sessionId, cwd, ...(initialization ? { initialization } : {}) },
|
|
146
|
+
inputs,
|
|
147
|
+
),
|
|
137
148
|
);
|
|
138
|
-
}),
|
|
149
|
+
}, true),
|
|
139
150
|
);
|
|
140
151
|
|
|
141
152
|
if (broker.askEnabled) {
|
|
@@ -158,7 +169,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
158
169
|
_meta: { ui: { resourceUri: questionTemplate } },
|
|
159
170
|
},
|
|
160
171
|
handle(async ({ sessionId, ...input }, context) => {
|
|
161
|
-
const question = await broker.ask(
|
|
172
|
+
const { initialization, ...question } = await broker.ask(
|
|
162
173
|
requireChatId(context),
|
|
163
174
|
sessionId,
|
|
164
175
|
input,
|
|
@@ -167,6 +178,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
167
178
|
);
|
|
168
179
|
const result = await finishResult(broker, context, {
|
|
169
180
|
content: [
|
|
181
|
+
...(initialization ? textResult({ initialization }).content : []),
|
|
170
182
|
{
|
|
171
183
|
type: "text",
|
|
172
184
|
text: `Question created. Call ask_assert({"questionId":"${question.id}"}) next.`,
|
|
@@ -226,7 +238,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
226
238
|
annotations: toolAnnotations,
|
|
227
239
|
_meta: { ui: { visibility: ["app"] }, "openai/widgetAccessible": true },
|
|
228
240
|
},
|
|
229
|
-
|
|
241
|
+
async ({ questionId, answer, loaded = false }, context) => {
|
|
230
242
|
const question = await broker.answer(
|
|
231
243
|
requireChatId(context),
|
|
232
244
|
questionId,
|
|
@@ -244,7 +256,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
244
256
|
content: [{ type: "text", text }],
|
|
245
257
|
structuredContent: { text, question },
|
|
246
258
|
};
|
|
247
|
-
}
|
|
259
|
+
},
|
|
248
260
|
);
|
|
249
261
|
|
|
250
262
|
server.registerResource(
|
|
@@ -308,7 +320,13 @@ export function createServer(broker: Broker): McpServer {
|
|
|
308
320
|
broker,
|
|
309
321
|
context,
|
|
310
322
|
textResult(
|
|
311
|
-
{
|
|
323
|
+
{
|
|
324
|
+
session: inspected.session,
|
|
325
|
+
tools: inspected.tools,
|
|
326
|
+
...(inspected.initialization
|
|
327
|
+
? { initialization: inspected.initialization }
|
|
328
|
+
: {}),
|
|
329
|
+
},
|
|
312
330
|
inputs,
|
|
313
331
|
),
|
|
314
332
|
);
|
|
@@ -356,6 +374,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
356
374
|
result.sessionId,
|
|
357
375
|
result.cwd,
|
|
358
376
|
result.inputs,
|
|
377
|
+
result.initialization,
|
|
359
378
|
),
|
|
360
379
|
);
|
|
361
380
|
}),
|
|
@@ -396,12 +415,93 @@ export function createServer(broker: Broker): McpServer {
|
|
|
396
415
|
result.sessionId,
|
|
397
416
|
result.cwd,
|
|
398
417
|
result.inputs,
|
|
418
|
+
result.initialization,
|
|
399
419
|
),
|
|
400
420
|
);
|
|
401
421
|
}),
|
|
402
422
|
);
|
|
403
423
|
}
|
|
404
424
|
|
|
425
|
+
server.registerTool(
|
|
426
|
+
"history",
|
|
427
|
+
{
|
|
428
|
+
title: "Session history",
|
|
429
|
+
description:
|
|
430
|
+
"Read recent Pi history with entry IDs and timestamps. Use before/after to page the current branch. History provides context for the current task. An explicit sessionId applies only to this read.",
|
|
431
|
+
inputSchema: historyInput.extend({
|
|
432
|
+
sessionId: z
|
|
433
|
+
.string()
|
|
434
|
+
.optional()
|
|
435
|
+
.describe("Pi session to read; defaults to this chat's session"),
|
|
436
|
+
}),
|
|
437
|
+
outputSchema,
|
|
438
|
+
annotations: toolAnnotations,
|
|
439
|
+
},
|
|
440
|
+
handle(async ({ sessionId, ...range }, context) => {
|
|
441
|
+
const { history, ...session } = await broker.history(
|
|
442
|
+
requireChatId(context),
|
|
443
|
+
sessionId,
|
|
444
|
+
range,
|
|
445
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
446
|
+
context.mcpReq.signal,
|
|
447
|
+
);
|
|
448
|
+
const { content, ...page } = history;
|
|
449
|
+
return formatResult({
|
|
450
|
+
content: [
|
|
451
|
+
...textResult({
|
|
452
|
+
...session,
|
|
453
|
+
history: page,
|
|
454
|
+
...(broker.syncEnabled
|
|
455
|
+
? { sync: broker.syncState(session.sessionId) }
|
|
456
|
+
: {}),
|
|
457
|
+
}).content,
|
|
458
|
+
...content,
|
|
459
|
+
],
|
|
460
|
+
});
|
|
461
|
+
}, true),
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
if (broker.syncEnabled) {
|
|
465
|
+
server.registerTool(
|
|
466
|
+
"sync",
|
|
467
|
+
{
|
|
468
|
+
title: "Synchronize Pi activity",
|
|
469
|
+
description:
|
|
470
|
+
"Pause conflicting activity. Verify your initialization code to coordinate; failed verification still permits discussion. The coordinator decides who continues, their tasks, who exits, and when to release.",
|
|
471
|
+
inputSchema: z.object({
|
|
472
|
+
action: z.enum(["start", "verify", "release"]),
|
|
473
|
+
code: z
|
|
474
|
+
.string()
|
|
475
|
+
.optional()
|
|
476
|
+
.describe(
|
|
477
|
+
"Initialization code for verification, or the verified code for release; omit when unavailable",
|
|
478
|
+
),
|
|
479
|
+
sessionId: z
|
|
480
|
+
.string()
|
|
481
|
+
.optional()
|
|
482
|
+
.describe(
|
|
483
|
+
"Pi session to synchronize; defaults to this chat's session",
|
|
484
|
+
),
|
|
485
|
+
}),
|
|
486
|
+
outputSchema,
|
|
487
|
+
annotations: toolAnnotations,
|
|
488
|
+
},
|
|
489
|
+
async ({ action, code, sessionId }, context) =>
|
|
490
|
+
formatResult(
|
|
491
|
+
textResult(
|
|
492
|
+
await broker.sync(
|
|
493
|
+
requireChatId(context),
|
|
494
|
+
sessionId,
|
|
495
|
+
action,
|
|
496
|
+
code,
|
|
497
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
498
|
+
context.mcpReq.signal,
|
|
499
|
+
),
|
|
500
|
+
),
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
405
505
|
server.registerTool(
|
|
406
506
|
"sessions",
|
|
407
507
|
{
|
|
@@ -420,12 +520,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
420
520
|
handle(async (args, context) => {
|
|
421
521
|
const chatId = requestChatId(context);
|
|
422
522
|
const inputs = chatId
|
|
423
|
-
? await broker.inputs(
|
|
424
|
-
chatId,
|
|
425
|
-
args.sessionId,
|
|
426
|
-
context.mcpReq._meta?.["otunnel/requestId"],
|
|
427
|
-
context.mcpReq.signal,
|
|
428
|
-
)
|
|
523
|
+
? await broker.inputs(chatId, args.sessionId, context.mcpReq.signal)
|
|
429
524
|
: [];
|
|
430
525
|
const result = textResult(
|
|
431
526
|
{
|
|
@@ -435,7 +530,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
435
530
|
inputs,
|
|
436
531
|
);
|
|
437
532
|
return finishResult(broker, context, result);
|
|
438
|
-
}),
|
|
533
|
+
}, true),
|
|
439
534
|
);
|
|
440
535
|
|
|
441
536
|
server.registerResource(
|
|
@@ -479,21 +574,32 @@ function textResult(
|
|
|
479
574
|
async function finishResult<
|
|
480
575
|
T extends { content: ReturnType<typeof toolResult>["content"] },
|
|
481
576
|
>(broker: Broker, context: RequestContext, result: T) {
|
|
577
|
+
context.mcpReq.signal.throwIfAborted();
|
|
482
578
|
const chatId = requestChatId(context);
|
|
483
|
-
|
|
579
|
+
if (!broker.deliveryAllowed(context.mcpReq.signal))
|
|
580
|
+
return formatResult(result);
|
|
581
|
+
const deliveries = chatId ? broker.deliveries(chatId) : [];
|
|
484
582
|
const answers = chatId ? broker.answers(chatId) : [];
|
|
485
583
|
const content = [
|
|
486
584
|
...result.content,
|
|
487
585
|
...deliveryContent(deliveries),
|
|
488
586
|
...answerContent(answers),
|
|
489
587
|
];
|
|
490
|
-
const structuredContent = {
|
|
491
|
-
text: content
|
|
492
|
-
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
493
|
-
.join("\n"),
|
|
494
|
-
};
|
|
495
588
|
await broker.acknowledge(deliveries, answers, context.mcpReq.signal);
|
|
496
|
-
return { ...result, content
|
|
589
|
+
return formatResult({ ...result, content });
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function formatResult<
|
|
593
|
+
T extends { content: ReturnType<typeof toolResult>["content"] },
|
|
594
|
+
>(result: T) {
|
|
595
|
+
return {
|
|
596
|
+
...result,
|
|
597
|
+
structuredContent: {
|
|
598
|
+
text: result.content
|
|
599
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
600
|
+
.join("\n"),
|
|
601
|
+
},
|
|
602
|
+
};
|
|
497
603
|
}
|
|
498
604
|
|
|
499
605
|
function requestChatId(context: RequestContext): string | undefined {
|
package/src/session.ts
CHANGED
|
@@ -13,7 +13,9 @@ import type {
|
|
|
13
13
|
SessionEntry,
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Text } from "@earendil-works/pi-tui";
|
|
16
|
+
import { type Activity, chatLabel, source } from "./activity.ts";
|
|
16
17
|
import type { DeliveryRecord } from "./delivery.ts";
|
|
18
|
+
import { historyResult } from "./history.ts";
|
|
17
19
|
import {
|
|
18
20
|
type BrokerMessage,
|
|
19
21
|
IpcClient,
|
|
@@ -27,7 +29,7 @@ import { readSessionResource, rememberImages } from "./resources.ts";
|
|
|
27
29
|
|
|
28
30
|
type RemoteRequest = Extract<BrokerMessage, { type: "chat" | "call" }>;
|
|
29
31
|
|
|
30
|
-
interface Notice {
|
|
32
|
+
interface Notice extends Activity {
|
|
31
33
|
message: string;
|
|
32
34
|
type: "info" | "warning" | "error";
|
|
33
35
|
}
|
|
@@ -126,9 +128,17 @@ export class LocalSession {
|
|
|
126
128
|
this.#pi.on("session_shutdown", () => this.close());
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
#notify(
|
|
131
|
+
#notify(
|
|
132
|
+
message: string,
|
|
133
|
+
type: Notice["type"] = "info",
|
|
134
|
+
activity: Activity = {},
|
|
135
|
+
): void {
|
|
130
136
|
if (this.#context)
|
|
131
|
-
this.#pi.appendEntry<Notice>("chappie.notice", {
|
|
137
|
+
this.#pi.appendEntry<Notice>("chappie.notice", {
|
|
138
|
+
message,
|
|
139
|
+
type,
|
|
140
|
+
...activity,
|
|
141
|
+
});
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
async start(output: ProviderOutput): Promise<void> {
|
|
@@ -261,7 +271,7 @@ export class LocalSession {
|
|
|
261
271
|
if (
|
|
262
272
|
message.sessionId === this.#context?.sessionManager.getSessionId()
|
|
263
273
|
) {
|
|
264
|
-
this.#notify(message.message);
|
|
274
|
+
this.#notify(message.message, "info", message.activity);
|
|
265
275
|
}
|
|
266
276
|
break;
|
|
267
277
|
case "inspect":
|
|
@@ -276,6 +286,26 @@ export class LocalSession {
|
|
|
276
286
|
};
|
|
277
287
|
});
|
|
278
288
|
break;
|
|
289
|
+
case "history":
|
|
290
|
+
await this.#reply(message.id, message.sessionId, () => {
|
|
291
|
+
const context = this.#context;
|
|
292
|
+
if (!context) throw new Error("Chappie session is not available");
|
|
293
|
+
const history = historyResult(
|
|
294
|
+
context.sessionManager.getBranch(),
|
|
295
|
+
message.sessionId,
|
|
296
|
+
message.range,
|
|
297
|
+
);
|
|
298
|
+
this.#notify(
|
|
299
|
+
`${chatLabel(message)} read history: ${history.count} entries`,
|
|
300
|
+
"info",
|
|
301
|
+
{
|
|
302
|
+
event: "history",
|
|
303
|
+
...source(message.chatId, message.requestId),
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
return { type: "result", id: message.id, cwd: context.cwd, history };
|
|
307
|
+
});
|
|
308
|
+
break;
|
|
279
309
|
case "ackInputs":
|
|
280
310
|
if (
|
|
281
311
|
message.sessionId === this.#context?.sessionManager.getSessionId()
|
|
@@ -305,8 +335,12 @@ export class LocalSession {
|
|
|
305
335
|
? [...new Set(request.calls.map((call) => call.name))].join(", ")
|
|
306
336
|
: "chat";
|
|
307
337
|
this.#notify(
|
|
308
|
-
`${name} cancelled for
|
|
338
|
+
`${name} cancelled for ${chatLabel(request)}: ${message.reason}`,
|
|
309
339
|
"warning",
|
|
340
|
+
{
|
|
341
|
+
event: "cancelled",
|
|
342
|
+
...source(request.chatId, request.requestId),
|
|
343
|
+
},
|
|
310
344
|
);
|
|
311
345
|
if (queued !== -1) {
|
|
312
346
|
this.#queue.splice(queued, 1);
|
|
@@ -370,6 +404,7 @@ export class LocalSession {
|
|
|
370
404
|
}
|
|
371
405
|
|
|
372
406
|
this.#queue.shift();
|
|
407
|
+
output.message.chappie = source(request.chatId, request.requestId);
|
|
373
408
|
this.#active = {
|
|
374
409
|
request,
|
|
375
410
|
session: this.#description(),
|
|
@@ -431,7 +466,7 @@ export class LocalSession {
|
|
|
431
466
|
if (active.cancelled !== undefined) {
|
|
432
467
|
const delivery: DeliveryRecord = {
|
|
433
468
|
id: randomUUID(),
|
|
434
|
-
chatId
|
|
469
|
+
...source(active.request.chatId, active.request.requestId),
|
|
435
470
|
sessionId: active.session.id,
|
|
436
471
|
cwd: active.session.cwd,
|
|
437
472
|
toolResults: active.toolResults,
|
|
@@ -515,7 +550,14 @@ export class LocalSession {
|
|
|
515
550
|
await connection.send({ type: "delivery", delivery });
|
|
516
551
|
await completion.promise;
|
|
517
552
|
this.#deliveries.delete(delivery.id);
|
|
518
|
-
this.#notify(
|
|
553
|
+
this.#notify(
|
|
554
|
+
`Result saved for ChatGPT ${delivery.chatId.slice(-4)}`,
|
|
555
|
+
"info",
|
|
556
|
+
{
|
|
557
|
+
event: "result_saved",
|
|
558
|
+
...source(delivery.chatId, delivery.requestId),
|
|
559
|
+
},
|
|
560
|
+
);
|
|
519
561
|
} finally {
|
|
520
562
|
this.#stores.delete(delivery.id);
|
|
521
563
|
}
|