@zetaloop/chappie 0.4.0 → 0.5.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 +3 -9
- package/docs/tools.md +36 -36
- package/package.json +1 -1
- package/src/broker.ts +106 -328
- package/src/config.ts +0 -1
- package/src/history.ts +15 -2
- package/src/index.ts +4 -1
- package/src/instructions.md +5 -5
- package/src/ipc.ts +19 -4
- package/src/questions.ts +6 -14
- package/src/resources.ts +26 -6
- package/src/server.ts +40 -89
- package/src/session.ts +272 -27
- package/src/state.ts +1 -17
- package/src/transfer.ts +200 -49
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. 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
|
|
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 globalAgents is present, read and follow the instructions at globalAgents.path. Follow initialization.instructions for your participation in the current task. When executing work, 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
|
|
|
@@ -8,10 +8,10 @@ Prefer ChatGPT's web search, connectors, and cloud tools for remote research and
|
|
|
8
8
|
|
|
9
9
|
Respond promptly to new Pi user input with a substantive reply, interaction, or immediate action that makes the response apparent in Pi before continuing lengthy work. Address inputs received together in one response; an immediate answer or result serves as its own acknowledgment.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
When executing work, use chat for assistant messages in Pi, including progress, explanations, and completion results. Use read, bash, edit, write, and transfer directly. init.tools is a Pi tool catalog; tools returns full definitions for call. Invoke Chappie's MCP tools directly. Each call array is one native Pi batch; separate calls are separate Pi turns. For Pi interaction, call an installed interactive tool.
|
|
12
12
|
|
|
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.
|
|
13
|
+
transfer pairs files from ChatGPT with Pi destination paths in order. To copy between Pi sessions, use paths on the selected source session and to: { sessionId, paths } for the destination. Each side resolves paths in its own working directory. Omit files and to 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
|
-
Tool results identify the executing Pi sessionId and cwd. A shell command can access another directory without changing its Pi session. structuredContent.text includes
|
|
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 tool output, new Pi input, webAnswer, and deferred results; images and resources are native content blocks. ChatGPT truncates tool responses exceeding 10,000 tokens. Continue from received results rather than repeating work. Host deadlines include queueing and execution; use local persistent processes for longer work.
|
|
16
16
|
|
|
17
|
-
Use history to read the current Pi branch with
|
|
17
|
+
Use history to read the current Pi branch with entry IDs and timestamps. History defaults to the last 20 entries; before pages backward and after pages forward. Use after with wait: true to follow progress: available entries return immediately, otherwise the call waits up to 30 seconds. An empty result means no new entries. Set observer: true when reading as an observer. Read the work's completion message to form the final reply. History returns saved contents separately from new Pi input.
|
package/src/ipc.ts
CHANGED
|
@@ -20,7 +20,8 @@ import type {
|
|
|
20
20
|
import type { Activity } from "./activity.ts";
|
|
21
21
|
import type { DeliveryRecord } from "./delivery.ts";
|
|
22
22
|
import type { HistoryRange, HistoryResult } from "./history.ts";
|
|
23
|
-
import type { ResourceData } from "./resources.ts";
|
|
23
|
+
import type { ResourceData, ResourceDescriptor } from "./resources.ts";
|
|
24
|
+
import type { TransferDetails } from "./transfer.ts";
|
|
24
25
|
|
|
25
26
|
const defaultPort = 24274;
|
|
26
27
|
|
|
@@ -50,7 +51,7 @@ export type SessionResult =
|
|
|
50
51
|
| {
|
|
51
52
|
inspection: SessionInspection;
|
|
52
53
|
inputs: SessionInput[];
|
|
53
|
-
globalAgents?: string;
|
|
54
|
+
globalAgents?: { path: string };
|
|
54
55
|
}
|
|
55
56
|
| { message: AssistantMessage; cwd: string; inputs: SessionInput[] }
|
|
56
57
|
| {
|
|
@@ -61,19 +62,32 @@ export type SessionResult =
|
|
|
61
62
|
}
|
|
62
63
|
| { history: HistoryResult; cwd: string }
|
|
63
64
|
| { resource: ResourceData }
|
|
65
|
+
| { transfer: TransferDetails }
|
|
64
66
|
| { error: string };
|
|
65
67
|
|
|
68
|
+
export type SessionRequest =
|
|
69
|
+
| { type: "inspect"; sessionId: string }
|
|
70
|
+
| { type: "readResource"; sessionId: string; uri: string; offset?: number }
|
|
71
|
+
| {
|
|
72
|
+
type: "copy";
|
|
73
|
+
sessionId: string;
|
|
74
|
+
resources: ResourceDescriptor[];
|
|
75
|
+
paths: string[];
|
|
76
|
+
overwrite?: boolean;
|
|
77
|
+
};
|
|
78
|
+
|
|
66
79
|
export type SessionMessage =
|
|
67
80
|
| { type: "sync"; id: number; session: SessionDescription }
|
|
68
81
|
| { type: "unregister"; sessionId: string }
|
|
69
82
|
| { type: "delivery"; delivery: DeliveryRecord }
|
|
83
|
+
| { type: "request"; id: number; request: SessionRequest }
|
|
84
|
+
| { type: "cancelRequest"; id: number }
|
|
70
85
|
| ({ type: "result"; id: number } & SessionResult);
|
|
71
86
|
|
|
72
87
|
export type BrokerMessage =
|
|
73
88
|
| { type: "synced"; id: number; sessionId: string }
|
|
74
89
|
| { type: "stored"; id: string }
|
|
75
90
|
| { type: "notice"; sessionId: string; message: string; activity?: Activity }
|
|
76
|
-
| { type: "inspect"; id: number; sessionId: string }
|
|
77
91
|
| {
|
|
78
92
|
type: "history";
|
|
79
93
|
id: number;
|
|
@@ -99,7 +113,8 @@ export type BrokerMessage =
|
|
|
99
113
|
calls: ToolCall[];
|
|
100
114
|
}
|
|
101
115
|
| { type: "cancel"; id: number; sessionId: string; reason: string }
|
|
102
|
-
|
|
|
116
|
+
| (SessionRequest & { id: number })
|
|
117
|
+
| ({ type: "response"; id: number } & SessionResult)
|
|
103
118
|
| { type: "ackInputs"; sessionId: string; ids: string[] };
|
|
104
119
|
|
|
105
120
|
export function ipcEndpoint(agentDir: string): string {
|
package/src/questions.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
|
|
3
3
|
export const questionInstructions =
|
|
4
|
-
"
|
|
4
|
+
"ask requests a question widget in ChatGPT; its result confirms creation of the request. Immediately call ask_assert with question.id to confirm loading. If the widget fails to load within 10 seconds, the assertion fails and records the question as skipped. Use an installed Pi interactive tool through call when an answer is needed. User answers arrive separately as webAnswer in normal tool results. Apply answers and revisions promptly; a user skip means proceed with available information.";
|
|
5
5
|
|
|
6
6
|
export const questionInput = z.object({
|
|
7
|
-
header: z
|
|
8
|
-
.string()
|
|
9
|
-
.trim()
|
|
10
|
-
.min(1)
|
|
11
|
-
.optional()
|
|
12
|
-
.describe("Short topic label above the question, when useful"),
|
|
7
|
+
header: z.string().trim().min(1).optional().describe("Short topic label"),
|
|
13
8
|
question: z
|
|
14
9
|
.string()
|
|
15
10
|
.trim()
|
|
@@ -18,7 +13,7 @@ export const questionInput = z.object({
|
|
|
18
13
|
context: z
|
|
19
14
|
.string()
|
|
20
15
|
.optional()
|
|
21
|
-
.describe("
|
|
16
|
+
.describe("Background needed to answer the question"),
|
|
22
17
|
options: z
|
|
23
18
|
.array(
|
|
24
19
|
z.object({
|
|
@@ -30,12 +25,12 @@ export const questionInput = z.object({
|
|
|
30
25
|
recommended: z
|
|
31
26
|
.boolean()
|
|
32
27
|
.optional()
|
|
33
|
-
.describe("
|
|
28
|
+
.describe("Preferred choice; place it first"),
|
|
34
29
|
}),
|
|
35
30
|
)
|
|
36
31
|
.default([])
|
|
37
32
|
.describe(
|
|
38
|
-
"Distinct choices, usually two or three.
|
|
33
|
+
"Distinct choices, usually two or three. Freeform answers and skipping are available separately.",
|
|
39
34
|
),
|
|
40
35
|
allowMultiple: z
|
|
41
36
|
.boolean()
|
|
@@ -46,10 +41,7 @@ export const questionInput = z.object({
|
|
|
46
41
|
export const answerInput = z.object({
|
|
47
42
|
selections: z.array(z.number().int().nonnegative()).default([]),
|
|
48
43
|
text: z.string().trim().default(""),
|
|
49
|
-
skipped: z
|
|
50
|
-
.literal(true)
|
|
51
|
-
.optional()
|
|
52
|
-
.describe("The user skipped this question"),
|
|
44
|
+
skipped: z.literal(true).optional().describe("The question was skipped"),
|
|
53
45
|
});
|
|
54
46
|
|
|
55
47
|
export const questionOutput = questionInput.extend({
|
package/src/resources.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { open, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
5
5
|
import mime from "mime";
|
|
@@ -124,15 +124,35 @@ export function describeResource(
|
|
|
124
124
|
export async function readSessionResource(
|
|
125
125
|
sessionId: string,
|
|
126
126
|
uri: string,
|
|
127
|
+
offset?: number,
|
|
127
128
|
): Promise<ResourceData> {
|
|
128
129
|
const descriptor = describeResource(sessionId, uri);
|
|
129
130
|
const entry = store(sessionId).get(uri);
|
|
130
131
|
if (!entry) throw new Error(`Unknown Chappie resource: ${uri}`);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
if (offset === undefined) {
|
|
133
|
+
const blob =
|
|
134
|
+
entry.type === "file"
|
|
135
|
+
? (await readFile(entry.path)).toString("base64")
|
|
136
|
+
: entry.data;
|
|
137
|
+
return { ...descriptor, blob };
|
|
138
|
+
}
|
|
139
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
140
|
+
throw new Error("Resource offset must be a nonnegative integer");
|
|
141
|
+
const length = Math.min(1024 * 1024, Math.max(0, descriptor.size - offset));
|
|
142
|
+
let data: Buffer;
|
|
143
|
+
if (entry.type === "file") {
|
|
144
|
+
await using file = await open(entry.path, "r");
|
|
145
|
+
const { buffer, bytesRead } = await file.read(
|
|
146
|
+
Buffer.alloc(length),
|
|
147
|
+
0,
|
|
148
|
+
length,
|
|
149
|
+
offset,
|
|
150
|
+
);
|
|
151
|
+
data = buffer.subarray(0, bytesRead);
|
|
152
|
+
} else {
|
|
153
|
+
data = Buffer.from(entry.data, "base64").subarray(offset, offset + length);
|
|
154
|
+
}
|
|
155
|
+
return { ...descriptor, blob: data.toString("base64") };
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
export function resourceSessionId(uri: string): string {
|
package/src/server.ts
CHANGED
|
@@ -32,9 +32,6 @@ const outputSchema = z.object({
|
|
|
32
32
|
),
|
|
33
33
|
});
|
|
34
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
|
-
|
|
38
35
|
const questionTemplate = "ui://chappie/question.html";
|
|
39
36
|
const questionSchema = outputSchema.extend({ question: questionOutput });
|
|
40
37
|
const toolAnnotations = {
|
|
@@ -61,27 +58,18 @@ export function createServer(broker: Broker): McpServer {
|
|
|
61
58
|
instructions: [
|
|
62
59
|
instructions,
|
|
63
60
|
...(broker.askEnabled ? [questionInstructions] : []),
|
|
64
|
-
...(broker.syncEnabled ? [syncInstructions] : []),
|
|
65
61
|
].join("\n\n"),
|
|
66
62
|
},
|
|
67
63
|
);
|
|
68
64
|
|
|
69
65
|
function handle<Args, Result>(
|
|
70
66
|
callback: (args: Args, context: RequestContext) => Promise<Result>,
|
|
71
|
-
communication = false,
|
|
72
67
|
) {
|
|
73
|
-
return (args: Args, context: RequestContext): Promise<Result> =>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
(signal) =>
|
|
79
|
-
callback(args, {
|
|
80
|
-
...context,
|
|
81
|
-
mcpReq: { ...context.mcpReq, signal },
|
|
82
|
-
}),
|
|
83
|
-
communication,
|
|
84
|
-
);
|
|
68
|
+
return (args: Args, context: RequestContext): Promise<Result> => {
|
|
69
|
+
requireChatId(context);
|
|
70
|
+
context.mcpReq.signal.throwIfAborted();
|
|
71
|
+
return callback(args, context);
|
|
72
|
+
};
|
|
85
73
|
}
|
|
86
74
|
|
|
87
75
|
server.registerTool(
|
|
@@ -89,7 +77,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
89
77
|
{
|
|
90
78
|
title: "Connect to Pi",
|
|
91
79
|
description:
|
|
92
|
-
"Select this chat's default Pi session and return its environment and
|
|
80
|
+
"Select this chat's default Pi session and return its environment, tool catalog, and participation instructions. 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.",
|
|
93
81
|
outputSchema,
|
|
94
82
|
inputSchema: z.object({
|
|
95
83
|
sessionId: z
|
|
@@ -115,8 +103,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
115
103
|
"chat",
|
|
116
104
|
{
|
|
117
105
|
title: "Reply in Pi",
|
|
118
|
-
description:
|
|
119
|
-
"Send an assistant message to Pi. Renders Markdown and saves the message in the session transcript.",
|
|
106
|
+
description: "Send a Markdown assistant message to Pi.",
|
|
120
107
|
outputSchema,
|
|
121
108
|
inputSchema: z.object({
|
|
122
109
|
text: z.string().min(1).describe("Assistant message in Markdown"),
|
|
@@ -146,7 +133,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
146
133
|
inputs,
|
|
147
134
|
),
|
|
148
135
|
);
|
|
149
|
-
}
|
|
136
|
+
}),
|
|
150
137
|
);
|
|
151
138
|
|
|
152
139
|
if (broker.askEnabled) {
|
|
@@ -155,7 +142,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
155
142
|
{
|
|
156
143
|
title: "Ask in ChatGPT",
|
|
157
144
|
description:
|
|
158
|
-
"
|
|
145
|
+
"Request a question widget in ChatGPT and return its ID immediately. Display depends on the host; call ask_assert next with question.id to confirm loading. Answers, revisions, and skips arrive as webAnswer in later tool results.",
|
|
159
146
|
inputSchema: questionInput.extend({
|
|
160
147
|
sessionId: z
|
|
161
148
|
.string()
|
|
@@ -181,7 +168,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
181
168
|
...(initialization ? textResult({ initialization }).content : []),
|
|
182
169
|
{
|
|
183
170
|
type: "text",
|
|
184
|
-
text: `Question
|
|
171
|
+
text: `Question widget requested. Call ask_assert({"questionId":"${question.id}"}) next.`,
|
|
185
172
|
},
|
|
186
173
|
],
|
|
187
174
|
});
|
|
@@ -197,7 +184,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
197
184
|
{
|
|
198
185
|
title: "Assert question display",
|
|
199
186
|
description:
|
|
200
|
-
"
|
|
187
|
+
"Confirm that an ask widget loaded in ChatGPT. Call immediately after ask with question.id. Fails after 10 seconds without loading and records the question as skipped. Use a Pi interactive tool if an answer is needed. User answers arrive separately as webAnswer.",
|
|
201
188
|
inputSchema: z.object({
|
|
202
189
|
questionId: z.string().describe("question.id returned by ask"),
|
|
203
190
|
}),
|
|
@@ -278,7 +265,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
278
265
|
csp: { connectDomains: [], resourceDomains: [] },
|
|
279
266
|
},
|
|
280
267
|
"openai/widgetDescription":
|
|
281
|
-
"A
|
|
268
|
+
"A question the user can answer or revise.",
|
|
282
269
|
},
|
|
283
270
|
},
|
|
284
271
|
],
|
|
@@ -427,7 +414,7 @@ export function createServer(broker: Broker): McpServer {
|
|
|
427
414
|
{
|
|
428
415
|
title: "Session history",
|
|
429
416
|
description:
|
|
430
|
-
"Read
|
|
417
|
+
"Read Pi history with entry IDs and timestamps. Use before/after to page the current branch, and wait to follow new progress when caught up. Set observer when reading as an observer. An explicit sessionId applies only to this read.",
|
|
431
418
|
inputSchema: historyInput.extend({
|
|
432
419
|
sessionId: z
|
|
433
420
|
.string()
|
|
@@ -446,62 +433,18 @@ export function createServer(broker: Broker): McpServer {
|
|
|
446
433
|
context.mcpReq.signal,
|
|
447
434
|
);
|
|
448
435
|
const { content, ...page } = history;
|
|
449
|
-
return formatResult(
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
...session,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
],
|
|
460
|
-
});
|
|
461
|
-
}, true),
|
|
436
|
+
return formatResult(
|
|
437
|
+
{
|
|
438
|
+
content: [
|
|
439
|
+
...textResult({ ...session, history: page }).content,
|
|
440
|
+
...content,
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
requireChatId(context),
|
|
444
|
+
);
|
|
445
|
+
}),
|
|
462
446
|
);
|
|
463
447
|
|
|
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
|
-
|
|
505
448
|
server.registerTool(
|
|
506
449
|
"sessions",
|
|
507
450
|
{
|
|
@@ -530,14 +473,17 @@ export function createServer(broker: Broker): McpServer {
|
|
|
530
473
|
inputs,
|
|
531
474
|
);
|
|
532
475
|
return finishResult(broker, context, result);
|
|
533
|
-
}
|
|
476
|
+
}),
|
|
534
477
|
);
|
|
535
478
|
|
|
536
479
|
server.registerResource(
|
|
537
480
|
"Pi resource",
|
|
538
|
-
new ResourceTemplate(
|
|
539
|
-
|
|
540
|
-
|
|
481
|
+
new ResourceTemplate(
|
|
482
|
+
"chappie://session/{sessionId}/{kind}/{id}/{name}{?chatId}",
|
|
483
|
+
{
|
|
484
|
+
list: undefined,
|
|
485
|
+
},
|
|
486
|
+
),
|
|
541
487
|
{ title: "Pi resource" },
|
|
542
488
|
async (uri, _variables, context) => {
|
|
543
489
|
const resource = await broker.readResource(
|
|
@@ -576,8 +522,6 @@ async function finishResult<
|
|
|
576
522
|
>(broker: Broker, context: RequestContext, result: T) {
|
|
577
523
|
context.mcpReq.signal.throwIfAborted();
|
|
578
524
|
const chatId = requestChatId(context);
|
|
579
|
-
if (!broker.deliveryAllowed(context.mcpReq.signal))
|
|
580
|
-
return formatResult(result);
|
|
581
525
|
const deliveries = chatId ? broker.deliveries(chatId) : [];
|
|
582
526
|
const answers = chatId ? broker.answers(chatId) : [];
|
|
583
527
|
const content = [
|
|
@@ -586,16 +530,23 @@ async function finishResult<
|
|
|
586
530
|
...answerContent(answers),
|
|
587
531
|
];
|
|
588
532
|
await broker.acknowledge(deliveries, answers, context.mcpReq.signal);
|
|
589
|
-
return formatResult({ ...result, content });
|
|
533
|
+
return formatResult({ ...result, content }, chatId);
|
|
590
534
|
}
|
|
591
535
|
|
|
592
536
|
function formatResult<
|
|
593
537
|
T extends { content: ReturnType<typeof toolResult>["content"] },
|
|
594
|
-
>(result: T) {
|
|
538
|
+
>(result: T, chatId?: string) {
|
|
539
|
+
const content = result.content.map((block) => {
|
|
540
|
+
if (block.type !== "resource_link" || !chatId) return block;
|
|
541
|
+
const uri = new URL(block.uri);
|
|
542
|
+
uri.searchParams.set("chatId", chatId);
|
|
543
|
+
return { ...block, uri: uri.href };
|
|
544
|
+
});
|
|
595
545
|
return {
|
|
596
546
|
...result,
|
|
547
|
+
content,
|
|
597
548
|
structuredContent: {
|
|
598
|
-
text:
|
|
549
|
+
text: content
|
|
599
550
|
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
600
551
|
.join("\n"),
|
|
601
552
|
},
|