@zetaloop/chappie 0.3.1 → 0.4.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 +11 -3
- package/docs/tools.md +50 -4
- package/package.json +1 -1
- package/src/activity.ts +21 -0
- package/src/broker.ts +476 -113
- package/src/config.ts +1 -1
- package/src/delivery.ts +3 -2
- package/src/history.ts +123 -0
- package/src/instructions.md +3 -1
- package/src/ipc.ts +14 -1
- package/src/provider.ts +2 -1
- package/src/server.ts +154 -31
- package/src/session.ts +49 -8
- package/src/state.ts +12 -17
- package/src/tools.ts +12 -2
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,6 @@
|
|
|
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
4
|
|
|
5
5
|
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
6
|
|
|
@@ -13,3 +13,5 @@ Use chat for assistant messages in Pi, including progress, explanations, and res
|
|
|
13
13
|
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
14
|
|
|
15
15
|
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.
|
|
16
|
+
|
|
17
|
+
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. Request IDs and the workflow suffix in notices describe recorded requests, and duplicate executions can share a workflow ID. It 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
|
+
"Synchronization ends accidental duplicate executions so one execution continues. Retain each initialization code privately in this ChatGPT context for its Pi session. On conflicting activity, call sync with action start, then verify with the code returned by your own most recent initialization. A rejected or unavailable code means this execution must send one final chat message with its task, entry time, and explicit exit statement, then end this ChatGPT response immediately. It must not wait for unlock, poll, reinitialize, or resume after release. Verification returns a new code and leaves ordinary tools and initialization locked. The verified execution uses chat and history to identify conflicting executions, require their final exit messages, and confirm their responses have ended before releasing with that code. Idle Pi status or a quiet history page is not proof of exit. Codes belong only in initialization and sync exchanges, never in chat, history, or notes.";
|
|
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 tool catalog. 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.",
|
|
92
|
+
"Select this chat's default Pi session and return its environment and tool catalog. 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
|
|
@@ -96,6 +104,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
96
104
|
const { inputs, ...initialized } = await broker.initialize(
|
|
97
105
|
chatId,
|
|
98
106
|
args.sessionId,
|
|
107
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
99
108
|
context.mcpReq.signal,
|
|
100
109
|
);
|
|
101
110
|
return finishResult(broker, context, textResult(initialized, inputs));
|
|
@@ -114,24 +123,30 @@ export function createServer(broker: Broker): McpServer {
|
|
|
114
123
|
sessionId: z
|
|
115
124
|
.string()
|
|
116
125
|
.optional()
|
|
117
|
-
.describe(
|
|
126
|
+
.describe(
|
|
127
|
+
"Pi session for this operation; becomes the default if none is set",
|
|
128
|
+
),
|
|
118
129
|
}),
|
|
119
130
|
annotations: toolAnnotations,
|
|
120
131
|
},
|
|
121
132
|
handle(async (args, context) => {
|
|
122
133
|
const chatId = requireChatId(context);
|
|
123
|
-
const { sessionId, cwd, inputs } = await broker.chat(
|
|
134
|
+
const { sessionId, cwd, inputs, initialization } = await broker.chat(
|
|
124
135
|
chatId,
|
|
125
136
|
args.sessionId,
|
|
126
137
|
args.text,
|
|
138
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
127
139
|
context.mcpReq.signal,
|
|
128
140
|
);
|
|
129
141
|
return finishResult(
|
|
130
142
|
broker,
|
|
131
143
|
context,
|
|
132
|
-
textResult(
|
|
144
|
+
textResult(
|
|
145
|
+
{ sessionId, cwd, ...(initialization ? { initialization } : {}) },
|
|
146
|
+
inputs,
|
|
147
|
+
),
|
|
133
148
|
);
|
|
134
|
-
}),
|
|
149
|
+
}, true),
|
|
135
150
|
);
|
|
136
151
|
|
|
137
152
|
if (broker.askEnabled) {
|
|
@@ -154,14 +169,16 @@ export function createServer(broker: Broker): McpServer {
|
|
|
154
169
|
_meta: { ui: { resourceUri: questionTemplate } },
|
|
155
170
|
},
|
|
156
171
|
handle(async ({ sessionId, ...input }, context) => {
|
|
157
|
-
const question = await broker.ask(
|
|
172
|
+
const { initialization, ...question } = await broker.ask(
|
|
158
173
|
requireChatId(context),
|
|
159
174
|
sessionId,
|
|
160
175
|
input,
|
|
176
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
161
177
|
context.mcpReq.signal,
|
|
162
178
|
);
|
|
163
179
|
const result = await finishResult(broker, context, {
|
|
164
180
|
content: [
|
|
181
|
+
...(initialization ? textResult({ initialization }).content : []),
|
|
165
182
|
{
|
|
166
183
|
type: "text",
|
|
167
184
|
text: `Question created. Call ask_assert({"questionId":"${question.id}"}) next.`,
|
|
@@ -221,7 +238,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
221
238
|
annotations: toolAnnotations,
|
|
222
239
|
_meta: { ui: { visibility: ["app"] }, "openai/widgetAccessible": true },
|
|
223
240
|
},
|
|
224
|
-
|
|
241
|
+
async ({ questionId, answer, loaded = false }, context) => {
|
|
225
242
|
const question = await broker.answer(
|
|
226
243
|
requireChatId(context),
|
|
227
244
|
questionId,
|
|
@@ -239,7 +256,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
239
256
|
content: [{ type: "text", text }],
|
|
240
257
|
structuredContent: { text, question },
|
|
241
258
|
};
|
|
242
|
-
}
|
|
259
|
+
},
|
|
243
260
|
);
|
|
244
261
|
|
|
245
262
|
server.registerResource(
|
|
@@ -285,7 +302,9 @@ export function createServer(broker: Broker): McpServer {
|
|
|
285
302
|
sessionId: z
|
|
286
303
|
.string()
|
|
287
304
|
.optional()
|
|
288
|
-
.describe(
|
|
305
|
+
.describe(
|
|
306
|
+
"Pi session for this operation; becomes the default if none is set",
|
|
307
|
+
),
|
|
289
308
|
}),
|
|
290
309
|
annotations: toolAnnotations,
|
|
291
310
|
},
|
|
@@ -294,13 +313,20 @@ export function createServer(broker: Broker): McpServer {
|
|
|
294
313
|
requireChatId(context),
|
|
295
314
|
args.sessionId,
|
|
296
315
|
args.names,
|
|
316
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
297
317
|
context.mcpReq.signal,
|
|
298
318
|
);
|
|
299
319
|
return finishResult(
|
|
300
320
|
broker,
|
|
301
321
|
context,
|
|
302
322
|
textResult(
|
|
303
|
-
{
|
|
323
|
+
{
|
|
324
|
+
session: inspected.session,
|
|
325
|
+
tools: inspected.tools,
|
|
326
|
+
...(inspected.initialization
|
|
327
|
+
? { initialization: inspected.initialization }
|
|
328
|
+
: {}),
|
|
329
|
+
},
|
|
304
330
|
inputs,
|
|
305
331
|
),
|
|
306
332
|
);
|
|
@@ -326,7 +352,9 @@ export function createServer(broker: Broker): McpServer {
|
|
|
326
352
|
sessionId: z
|
|
327
353
|
.string()
|
|
328
354
|
.optional()
|
|
329
|
-
.describe(
|
|
355
|
+
.describe(
|
|
356
|
+
"Pi session for this operation; becomes the default if none is set",
|
|
357
|
+
),
|
|
330
358
|
}),
|
|
331
359
|
annotations: toolAnnotations,
|
|
332
360
|
},
|
|
@@ -335,6 +363,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
335
363
|
requireChatId(context),
|
|
336
364
|
args.sessionId,
|
|
337
365
|
args.calls,
|
|
366
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
338
367
|
context.mcpReq.signal,
|
|
339
368
|
);
|
|
340
369
|
return finishResult(
|
|
@@ -345,6 +374,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
345
374
|
result.sessionId,
|
|
346
375
|
result.cwd,
|
|
347
376
|
result.inputs,
|
|
377
|
+
result.initialization,
|
|
348
378
|
),
|
|
349
379
|
);
|
|
350
380
|
}),
|
|
@@ -374,6 +404,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
374
404
|
requireChatId(context),
|
|
375
405
|
sessionId,
|
|
376
406
|
calls,
|
|
407
|
+
context.mcpReq._meta?.["otunnel/requestId"],
|
|
377
408
|
context.mcpReq.signal,
|
|
378
409
|
);
|
|
379
410
|
return finishResult(
|
|
@@ -384,12 +415,93 @@ export function createServer(broker: Broker): McpServer {
|
|
|
384
415
|
result.sessionId,
|
|
385
416
|
result.cwd,
|
|
386
417
|
result.inputs,
|
|
418
|
+
result.initialization,
|
|
387
419
|
),
|
|
388
420
|
);
|
|
389
421
|
}),
|
|
390
422
|
);
|
|
391
423
|
}
|
|
392
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
|
+
"End duplicate executions: start synchronization, verify your own latest initialization code, and release after conflicting executions exit. Failed verification requires a final chat exit message followed by ending this response. Verification leaves the session locked; chat and history support exit coordination.",
|
|
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
|
+
|
|
393
505
|
server.registerTool(
|
|
394
506
|
"sessions",
|
|
395
507
|
{
|
|
@@ -418,7 +530,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
418
530
|
inputs,
|
|
419
531
|
);
|
|
420
532
|
return finishResult(broker, context, result);
|
|
421
|
-
}),
|
|
533
|
+
}, true),
|
|
422
534
|
);
|
|
423
535
|
|
|
424
536
|
server.registerResource(
|
|
@@ -462,21 +574,32 @@ function textResult(
|
|
|
462
574
|
async function finishResult<
|
|
463
575
|
T extends { content: ReturnType<typeof toolResult>["content"] },
|
|
464
576
|
>(broker: Broker, context: RequestContext, result: T) {
|
|
577
|
+
context.mcpReq.signal.throwIfAborted();
|
|
465
578
|
const chatId = requestChatId(context);
|
|
466
|
-
|
|
579
|
+
if (!broker.deliveryAllowed(context.mcpReq.signal))
|
|
580
|
+
return formatResult(result);
|
|
581
|
+
const deliveries = chatId ? broker.deliveries(chatId) : [];
|
|
467
582
|
const answers = chatId ? broker.answers(chatId) : [];
|
|
468
583
|
const content = [
|
|
469
584
|
...result.content,
|
|
470
585
|
...deliveryContent(deliveries),
|
|
471
586
|
...answerContent(answers),
|
|
472
587
|
];
|
|
473
|
-
const structuredContent = {
|
|
474
|
-
text: content
|
|
475
|
-
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
476
|
-
.join("\n"),
|
|
477
|
-
};
|
|
478
588
|
await broker.acknowledge(deliveries, answers, context.mcpReq.signal);
|
|
479
|
-
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
|
+
};
|
|
480
603
|
}
|
|
481
604
|
|
|
482
605
|
function requestChatId(context: RequestContext): string | undefined {
|