@zetaloop/chappie 0.2.0 → 0.2.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 CHANGED
@@ -25,7 +25,7 @@ Start otunnel with this configuration and add its tunnel as a developer-mode app
25
25
  pi --provider chappie --model chatgpt
26
26
  ```
27
27
 
28
- Open Pi with the Chappie provider, then ask ChatGPT to call `init`. Chappie pairs the chat with an online Pi session; the first remote operation starts its turn.
28
+ Open Pi with the Chappie provider, then ask ChatGPT to call `init`. A chat without a default is paired with the first online, unbound Pi session. To resume work in a new chat or branch, use `init` with the original Pi session ID; `sessions` lists projects when the target ID is unknown.
29
29
 
30
30
  ## Usage
31
31
 
package/docs/tools.md CHANGED
@@ -5,7 +5,7 @@
5
5
  | `init` | Connect to a Pi session and read its environment, tool catalog, skills, global `AGENTS.md`, and pending input. |
6
6
  | `sessions` | List connected Pi sessions and the chat's default session. |
7
7
  | `tools` | Read complete definitions for selected active tools. |
8
- | `chat` | Send an assistant message to Pi. |
8
+ | `chat` | Display Markdown as an assistant message in Pi. |
9
9
  | `call` | Run one or more tools as a Pi batch. |
10
10
  | `read` | Read local text or images. |
11
11
  | `bash` | Execute a shell command. |
@@ -23,16 +23,20 @@ Use `chat` for progress or results that should appear in Pi. When a Pi user deci
23
23
 
24
24
  ## Sessions
25
25
 
26
- Call `init` with `{}` to reuse the chat's session or pair with the first online, unbound Pi session. A newly opened Pi session can be selected before its first user message; the first remote operation starts its Chappie provider turn.
26
+ For a new task without a specific target, call `init` with `{}` to reuse this chat's default or allocate the first online, unbound Pi session. Sessions register while `chappie/chatgpt` is selected. They can be blank or already contain a task; a remote operation starts a turn when Pi is idle.
27
27
 
28
- `sessions` lists session IDs, working directories, names, and status. `ready` means the provider is accepting output, `executing` means Pi is handling an operation, and `idle` means the next operation will start a turn.
28
+ `sessions` lists session IDs, working directories, names, status, and `bindingCount`, the number of saved chat defaults pointing to each session. Zero means the session can be allocated automatically. The count includes closed chats; execution status describes Pi activity: `ready` accepts provider output, `executing` handles an operation, and `idle` starts a turn on the next operation.
29
29
 
30
- Use an ID from that list to select a default with `init`:
30
+ `init.selection` reports how the target was chosen: `existing` reuses this chat's default, `explicit` uses the supplied session ID, and `automatic` allocates the first online session with no saved bindings. Explicit selection also accepts sessions already used by other chats.
31
+
32
+ To continue existing work in a new chat or branch, pass the Pi session ID associated with that task in the inherited context:
31
33
 
32
34
  ```json
33
35
  { "sessionId": "<session-id>" }
34
36
  ```
35
37
 
38
+ This establishes the new chat's default, even when another chat already uses the same Pi session. For a requested project or session without a known ID, select it from `sessions` by working directory or name. When the target is absent or ambiguous, clarify the intended session before running tools. A session being the only one online does not establish that it is the requested target.
39
+
36
40
  The optional `sessionId` on other tools selects a session for that operation. For example, `read` can inspect another project:
37
41
 
38
42
  ```json
@@ -45,7 +49,7 @@ Several chats can select the same Pi session, and one chat can address several s
45
49
 
46
50
  ## Tool calls
47
51
 
48
- `read`, `bash`, `edit`, and `write` accept Pi's tool parameters plus `sessionId`. Their descriptions provide the current schemas. `init` lists every active tool by name with a short description. Load complete definitions for installed extension tools before calling them:
52
+ `read`, `bash`, `edit`, and `write` accept Pi's tool parameters plus `sessionId`. Their descriptions provide the current schemas. The catalog in `init.tools` lists Pi's native and extension tools available through `call`. Chappie's `init`, `sessions`, `tools`, and `chat` are separate top-level MCP tools. Load complete definitions for installed extension tools before calling them:
49
53
 
50
54
  ```json
51
55
  { "names": ["ask_user", "ctx_search"] }
@@ -66,7 +70,9 @@ A single extension tool uses a one-item `calls` array. To request a batch:
66
70
 
67
71
  Each batch returns its results together. Pi determines how its tools run within the batch. Separate calls run in order within one Pi session; different sessions can work independently.
68
72
 
69
- Extension tools execute through Pi, including their interactive prompts. Results include each tool's name, call ID, error status, and original text or image content.
73
+ Extension tools execute through Pi, including their interactive prompts. Each batch starts with its executing `sessionId` and Pi `cwd`, followed by each tool's name, call ID, error status, and original text or image content. The directory is captured when the batch starts; a shell `cd` changes that command's working directory, while the operation stays in the same Pi session.
74
+
75
+ Every tool declares a `{ text: string }` output. `structuredContent.text` contains the complete text in result order, including Pi input, deferred results, and image references. The same text remains in `content` alongside native images and resource links.
70
76
 
71
77
  ChatGPT file inputs use the direct `transfer` tool. Its top-level `files` parameter lets the host prepare the files before sending them to Pi.
72
78
 
@@ -78,11 +84,11 @@ Call `chat` to display a reply in Pi:
78
84
  { "text": "Updated the parser and its callers." }
79
85
  ```
80
86
 
81
- Each call completes one assistant message. Later operations start another turn when Pi is idle. The result returns the target session and any new Pi input without repeating the message text.
87
+ Pi renders the supplied Markdown, including fenced code blocks, and appends the message to the session transcript. Each call completes one assistant message. Later operations start another turn when Pi is idle. The result returns the target `sessionId`, Pi `cwd`, and any new Pi input without repeating the message text.
82
88
 
83
89
  User messages consumed by Pi accompany later Chappie replies, including images. Steering is delivered when Pi consumes it; follow-up uses Pi's normal follow-up timing.
84
90
 
85
- Explicit cancellation removes a queued request or asks Pi to stop its active batch. Available results from that batch accompany a later reply to the originating chat. `sessions` can retrieve them before another tool call. When ChatGPT stops without sending cancellation, local execution continues.
91
+ Explicit cancellation removes a queued request or asks Pi to stop its active batch. Available results from that batch accompany a later reply to the originating chat, with the original session ID and working directory. `sessions` can retrieve them before another tool call. When ChatGPT stops without sending cancellation, local execution continues.
86
92
 
87
93
  Host request deadlines include time spent in the queue. Use local facilities such as tmux for work intended to outlive one call.
88
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zetaloop/chappie",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Connect ChatGPT to Pi",
5
5
  "devDependencies": {
6
6
  "@earendil-works/pi-ai": "^0.85.1",
package/src/broker.ts CHANGED
@@ -42,6 +42,7 @@ interface ChangeWaiter {
42
42
  }
43
43
 
44
44
  export interface InitializedSession extends Omit<SessionInspection, "tools"> {
45
+ selection: "existing" | "explicit" | "automatic";
45
46
  globalAgents?: string;
46
47
  inputs: SessionInput[];
47
48
  tools: { name: string; description: string }[];
@@ -53,11 +54,13 @@ export interface InspectedSession extends SessionInspection {
53
54
 
54
55
  export interface ChatResult {
55
56
  sessionId: string;
57
+ cwd: string;
56
58
  inputs: SessionInput[];
57
59
  }
58
60
 
59
61
  export interface CallResult {
60
62
  sessionId: string;
63
+ cwd: string;
61
64
  toolResults: ToolResultMessage[];
62
65
  inputs: SessionInput[];
63
66
  }
@@ -101,12 +104,16 @@ export class Broker {
101
104
  await this.#ipc.close();
102
105
  }
103
106
 
104
- listSessions(sessionId?: string): SessionDescription[] {
105
- if (sessionId) {
106
- const session = this.#sessions.get(sessionId);
107
- return session ? [session.description] : [];
108
- }
109
- return [...this.#sessions.values()].map(({ description }) => description);
107
+ listSessions(
108
+ sessionId?: string,
109
+ ): (SessionDescription & { bindingCount: number })[] {
110
+ const counts = this.#state.bindingCounts();
111
+ return [...this.#sessions.values()]
112
+ .filter(({ description }) => !sessionId || description.id === sessionId)
113
+ .map(({ description }) => ({
114
+ ...description,
115
+ bindingCount: counts.get(description.id) ?? 0,
116
+ }));
110
117
  }
111
118
 
112
119
  binding(chatId: string): string | undefined {
@@ -118,11 +125,17 @@ export class Broker {
118
125
  sessionId: string | undefined,
119
126
  signal: AbortSignal,
120
127
  ): Promise<InitializedSession> {
121
- const target = await this.#selectSession(chatId, sessionId, signal, true);
128
+ const { sessionId: target, selection } = await this.#selectSession(
129
+ chatId,
130
+ sessionId,
131
+ signal,
132
+ true,
133
+ );
122
134
  const { inspection, inputs } = await this.#inspect(target, signal);
123
135
  const globalAgents = await this.#readGlobalAgents();
124
136
  await this.#ackInputs(target, inputs);
125
137
  return {
138
+ selection,
126
139
  ...inspection,
127
140
  tools: inspection.tools.map(({ name, description }) => ({
128
141
  name,
@@ -139,7 +152,12 @@ export class Broker {
139
152
  text: string,
140
153
  signal: AbortSignal,
141
154
  ): Promise<ChatResult> {
142
- const target = await this.#selectSession(chatId, sessionId, signal, false);
155
+ const { sessionId: target } = await this.#selectSession(
156
+ chatId,
157
+ sessionId,
158
+ signal,
159
+ false,
160
+ );
143
161
  const result = await this.#request(
144
162
  target,
145
163
  (id) => ({ type: "chat", id, chatId, sessionId: target, text }),
@@ -147,7 +165,7 @@ export class Broker {
147
165
  );
148
166
  if ("message" in result) {
149
167
  await this.#ackInputs(target, result.inputs);
150
- return { sessionId: target, inputs: result.inputs };
168
+ return { sessionId: target, cwd: result.cwd, inputs: result.inputs };
151
169
  }
152
170
  throw new Error("Pi session returned no assistant message");
153
171
  }
@@ -158,7 +176,12 @@ export class Broker {
158
176
  names: string[] | undefined,
159
177
  signal: AbortSignal,
160
178
  ): Promise<InspectedSession> {
161
- const target = await this.#selectSession(chatId, sessionId, signal, false);
179
+ const { sessionId: target } = await this.#selectSession(
180
+ chatId,
181
+ sessionId,
182
+ signal,
183
+ false,
184
+ );
162
185
  const { inspection, inputs } = await this.#inspect(target, signal);
163
186
  await this.#ackInputs(target, inputs);
164
187
  const selected = names ? new Set(names) : undefined;
@@ -177,7 +200,12 @@ export class Broker {
177
200
  calls: ToolInput[],
178
201
  signal: AbortSignal,
179
202
  ): Promise<CallResult> {
180
- const target = await this.#selectSession(chatId, sessionId, signal, false);
203
+ const { sessionId: target } = await this.#selectSession(
204
+ chatId,
205
+ sessionId,
206
+ signal,
207
+ false,
208
+ );
181
209
  const toolCalls: ToolCall[] = calls.map((call) => ({
182
210
  type: "toolCall",
183
211
  id: `chappie-${randomUUID()}`,
@@ -199,6 +227,7 @@ export class Broker {
199
227
  await this.#ackInputs(target, result.inputs);
200
228
  return {
201
229
  sessionId: target,
230
+ cwd: result.cwd,
202
231
  toolResults: result.toolResults,
203
232
  inputs: result.inputs,
204
233
  };
@@ -292,31 +321,34 @@ export class Broker {
292
321
  requestedId: string | undefined,
293
322
  signal: AbortSignal,
294
323
  bindRequested: boolean,
295
- ): Promise<string> {
324
+ ): Promise<{
325
+ sessionId: string;
326
+ selection: InitializedSession["selection"];
327
+ }> {
296
328
  if (requestedId) {
297
329
  await this.#waitForSession(requestedId, signal);
298
330
  if (bindRequested && this.#state.binding(chatId) !== requestedId) {
299
331
  await this.#state.setBinding(chatId, requestedId);
300
332
  this.#notifyChange();
301
333
  }
302
- return requestedId;
334
+ return { sessionId: requestedId, selection: "explicit" };
303
335
  }
304
336
 
305
337
  const boundId = this.#state.binding(chatId);
306
338
  if (boundId) {
307
339
  await this.#waitForSession(boundId, signal);
308
- return boundId;
340
+ return { sessionId: boundId, selection: "existing" };
309
341
  }
310
342
 
311
343
  for (;;) {
312
- const occupied = this.#state.boundSessions();
344
+ const occupied = this.#state.bindingCounts();
313
345
  const candidate = [...this.#sessions.keys()].find(
314
346
  (sessionId) => !occupied.has(sessionId),
315
347
  );
316
348
  if (candidate) {
317
349
  await this.#state.setBinding(chatId, candidate);
318
350
  this.#notifyChange();
319
- return candidate;
351
+ return { sessionId: candidate, selection: "automatic" };
320
352
  }
321
353
  await this.#waitForChange(signal);
322
354
  }
package/src/delivery.ts CHANGED
@@ -9,6 +9,7 @@ export interface DeliveryRecord {
9
9
  id: string;
10
10
  chatId: string;
11
11
  sessionId: string;
12
+ cwd: string;
12
13
  sessionFile?: string;
13
14
  toolCallIds: string[];
14
15
  inlineResults?: ToolResultMessage[];
@@ -57,6 +58,7 @@ export function deliveryContent(deliveries: ResolvedDelivery[]) {
57
58
  text: JSON.stringify({
58
59
  deferredResult: delivery.id,
59
60
  sessionId: delivery.sessionId,
61
+ cwd: delivery.cwd,
60
62
  error: delivery.error,
61
63
  }),
62
64
  },
@@ -1,11 +1,13 @@
1
- Chappie connects this ChatGPT conversation to local Pi sessions. Call init before starting work. Omitting sessionId reuses the current binding or pairs with the first online, unbound Pi session; specifying sessionId on init selects that Pi session as the new default. A sessionId on any other tool affects only that operation. Use sessions to inspect connected sessions without changing the binding.
1
+ Chappie connects this ChatGPT conversation to local Pi sessions. To continue existing work, call init with the Pi sessionId associated with that task in the context. A new chat or branch establishes its own binding to that ID. For a requested project or session without a known ID, use sessions to match its cwd or name, then init with the chosen ID. If the intended target is absent or ambiguous, resolve the selection with the user before starting work. Pi sessions register while the chappie/chatgpt provider is selected.
2
+
3
+ For work without a specific target, init without sessionId reuses the current binding or allocates the first online session with bindingCount zero. These sessions may be blank or already contain a task. Saved bindings persist across broker restarts; explicit init(sessionId) can share a session already used by another chat. init reports its selection as existing, explicit, or automatic. A sessionId on any other tool affects only that operation.
2
4
 
3
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.
4
6
 
5
7
  Prefer ChatGPT's web search, connectors, and cloud tools for remote research and cloud-side work. Use Chappie for local files, processes, Pi extensions, and Pi user interfaces. Pi project-memory tools operate on their local stores; Pi context-reduction tools do not change this ChatGPT conversation.
6
8
 
7
- Use chat to send progress and final messages that should appear in Pi. Use read, bash, edit, and write directly. init lists active tools with short descriptions; call tools with their names to load complete definitions before using other Pi tools through call. Use an installed interactive tool through call when input is needed in Pi. Separate calls remain separate Pi turns; use a call array when tools should share a batch.
9
+ Use chat to display Markdown progress and final messages in Pi, including code examples. Use read, bash, edit, and write directly. init.tools lists Pi's native and extension tools for call; Chappie's init, sessions, tools, and chat are separate top-level MCP tools. Use tools with names to load complete definitions before calling other Pi tools. Use an installed interactive tool through call when input is needed in Pi. Separate calls remain separate Pi turns; use a call array when tools should share a batch.
8
10
 
9
11
  Use transfer with paths and files to copy ChatGPT files into Pi. Omit files to expose existing Pi paths or chappie:// image references as MCP resources. Relative paths use the Pi working directory, existing targets require overwrite: true, and resource materialization may require host confirmation.
10
12
 
11
- Tool replies may contain user input consumed by Pi or results from an earlier explicitly cancelled request. Continue from those results instead of repeating completed work. Host request deadlines include queueing and execution; use local persistent-process facilities for work intended to outlive one request.
13
+ Tool replies identify the executing sessionId and Pi cwd. A shell command can access another directory while its session and transcript remain the same. Replies provide their complete text in structuredContent.text, including user input consumed by Pi and results from an earlier explicitly cancelled request. Images and file resources accompany the text as native content blocks. Continue from those results instead of repeating completed work. Host request deadlines include queueing and execution; use local persistent-process facilities for work intended to outlive one request.
package/src/ipc.ts CHANGED
@@ -44,9 +44,10 @@ export interface SessionInput {
44
44
 
45
45
  export type SessionResult =
46
46
  | { inspection: SessionInspection; inputs: SessionInput[] }
47
- | { message: AssistantMessage; inputs: SessionInput[] }
47
+ | { message: AssistantMessage; cwd: string; inputs: SessionInput[] }
48
48
  | {
49
49
  message: AssistantMessage;
50
+ cwd: string;
50
51
  toolResults: ToolResultMessage[];
51
52
  inputs: SessionInput[];
52
53
  }
package/src/server.ts CHANGED
@@ -16,6 +16,14 @@ const instructions = readFileSync(
16
16
  "utf8",
17
17
  ).trim();
18
18
 
19
+ const outputSchema = z.object({
20
+ text: z
21
+ .string()
22
+ .describe(
23
+ "Complete text output, including Pi user input and deferred results. Images and file resources accompany it as native content blocks.",
24
+ ),
25
+ });
26
+
19
27
  interface RequestContext {
20
28
  mcpReq: {
21
29
  _meta?: Record<string, unknown>;
@@ -37,14 +45,18 @@ export function createServer(broker: Broker): McpServer {
37
45
  {
38
46
  title: "Connect to Pi",
39
47
  description:
40
- "Connect this ChatGPT conversation to an online Pi session. A sessionId on init becomes the new default.",
48
+ "Bind this chat to a Pi session. To resume work or a ChatGPT branch, pass that task's Pi sessionId from context; use sessions to locate a requested target when its ID is unknown. Omit sessionId to reuse this chat's default or allocate the first online, unbound session. Returns the selection source.",
49
+ outputSchema,
41
50
  inputSchema: z.object({
42
51
  sessionId: z
43
52
  .string()
44
53
  .optional()
45
- .describe("Pi session to select explicitly"),
54
+ .describe(
55
+ "Pi session ID to set as this chat's default, including an already-bound session when resuming work",
56
+ ),
46
57
  }),
47
58
  annotations: {
59
+ readOnlyHint: false,
48
60
  destructiveHint: false,
49
61
  openWorldHint: false,
50
62
  },
@@ -64,28 +76,38 @@ export function createServer(broker: Broker): McpServer {
64
76
  "chat",
65
77
  {
66
78
  title: "Reply in Pi",
67
- description: "Display one complete assistant message in Pi.",
79
+ description:
80
+ "Display the supplied Markdown in Pi and append it to the session transcript. Code blocks are displayed as text.",
81
+ outputSchema,
68
82
  inputSchema: z.object({
69
- text: z.string().min(1).describe("Assistant message to display in Pi"),
83
+ text: z
84
+ .string()
85
+ .min(1)
86
+ .describe("Markdown message, including prose and code examples"),
70
87
  sessionId: z
71
88
  .string()
72
89
  .optional()
73
90
  .describe("Pi session for this operation only"),
74
91
  }),
75
92
  annotations: {
93
+ readOnlyHint: false,
76
94
  destructiveHint: false,
77
95
  openWorldHint: false,
78
96
  },
79
97
  },
80
98
  async (args, context) => {
81
99
  const chatId = requireChatId(context);
82
- const { sessionId, inputs } = await broker.chat(
100
+ const { sessionId, cwd, inputs } = await broker.chat(
83
101
  chatId,
84
102
  args.sessionId,
85
103
  args.text,
86
104
  context.mcpReq.signal,
87
105
  );
88
- return finishResult(broker, context, textResult({ sessionId }, inputs));
106
+ return finishResult(
107
+ broker,
108
+ context,
109
+ textResult({ sessionId, cwd }, inputs),
110
+ );
89
111
  },
90
112
  );
91
113
 
@@ -94,7 +116,8 @@ export function createServer(broker: Broker): McpServer {
94
116
  {
95
117
  title: "Pi tools",
96
118
  description:
97
- "Return complete definitions for active Pi tools. Provide names to inspect only those tools.",
119
+ "Return complete definitions for active Pi tools invoked through call. Provide names to inspect selected tools. Chappie's own MCP controls, including init, sessions, and chat, are exposed separately.",
120
+ outputSchema,
98
121
  inputSchema: z.object({
99
122
  names: z
100
123
  .array(z.string())
@@ -137,6 +160,7 @@ export function createServer(broker: Broker): McpServer {
137
160
  title: "Call Pi tools",
138
161
  description:
139
162
  "Execute one or more active Pi tools as one native batch. Arguments must match definitions returned by tools.",
163
+ outputSchema,
140
164
  inputSchema: z.object({
141
165
  calls: z
142
166
  .array(
@@ -152,6 +176,7 @@ export function createServer(broker: Broker): McpServer {
152
176
  .describe("Pi session for this operation only"),
153
177
  }),
154
178
  annotations: {
179
+ readOnlyHint: false,
155
180
  destructiveHint: true,
156
181
  openWorldHint: true,
157
182
  },
@@ -166,7 +191,12 @@ export function createServer(broker: Broker): McpServer {
166
191
  return finishResult(
167
192
  broker,
168
193
  context,
169
- toolResult(result.toolResults, result.sessionId, result.inputs),
194
+ toolResult(
195
+ result.toolResults,
196
+ result.sessionId,
197
+ result.cwd,
198
+ result.inputs,
199
+ ),
170
200
  );
171
201
  },
172
202
  );
@@ -177,6 +207,7 @@ export function createServer(broker: Broker): McpServer {
177
207
  {
178
208
  title: tool.name,
179
209
  description: tool.description,
210
+ outputSchema,
180
211
  inputSchema: tool.inputSchema,
181
212
  annotations: {
182
213
  readOnlyHint: tool.name === "read",
@@ -204,7 +235,12 @@ export function createServer(broker: Broker): McpServer {
204
235
  return finishResult(
205
236
  broker,
206
237
  context,
207
- toolResult(result.toolResults, result.sessionId, result.inputs),
238
+ toolResult(
239
+ result.toolResults,
240
+ result.sessionId,
241
+ result.cwd,
242
+ result.inputs,
243
+ ),
208
244
  );
209
245
  },
210
246
  );
@@ -215,7 +251,8 @@ export function createServer(broker: Broker): McpServer {
215
251
  {
216
252
  title: "Local sessions",
217
253
  description:
218
- "List online Pi sessions and the current conversation binding without waiting for offline sessions.",
254
+ "List online Pi sessions, their saved binding counts, and this conversation's default. A zero bindingCount permits automatic pairing; status describes execution. Offline sessions do not delay the listing.",
255
+ outputSchema,
219
256
  inputSchema: z.object({
220
257
  sessionId: z
221
258
  .string()
@@ -241,7 +278,7 @@ export function createServer(broker: Broker): McpServer {
241
278
  },
242
279
  inputs,
243
280
  );
244
- return chatId ? finishResult(broker, context, result) : result;
281
+ return finishResult(broker, context, result);
245
282
  },
246
283
  );
247
284
 
@@ -283,19 +320,19 @@ function textResult(
283
320
  };
284
321
  }
285
322
 
286
- async function finishResult<T extends { content: object[] }>(
287
- broker: Broker,
288
- context: RequestContext,
289
- result: T,
290
- ): Promise<T> {
291
- const chatId = requireChatId(context);
292
- const deliveries = await broker.deliveries(chatId);
293
- const completed = {
294
- ...result,
295
- content: [...result.content, ...deliveryContent(deliveries)],
296
- } as T;
323
+ async function finishResult<
324
+ T extends { content: ReturnType<typeof toolResult>["content"] },
325
+ >(broker: Broker, context: RequestContext, result: T) {
326
+ const chatId = requestChatId(context);
327
+ const deliveries = chatId ? await broker.deliveries(chatId) : [];
328
+ const content = [...result.content, ...deliveryContent(deliveries)];
329
+ const structuredContent = {
330
+ text: content
331
+ .flatMap((block) => (block.type === "text" ? [block.text] : []))
332
+ .join("\n"),
333
+ };
297
334
  await broker.acknowledgeDeliveries(deliveries, context.mcpReq.signal);
298
- return completed;
335
+ return { ...result, content, structuredContent };
299
336
  }
300
337
 
301
338
  function requestChatId(context: RequestContext): string | undefined {
package/src/session.ts CHANGED
@@ -35,6 +35,7 @@ interface StoreRequest {
35
35
 
36
36
  interface ActiveRequest {
37
37
  request: RemoteRequest;
38
+ session: SessionDescription;
38
39
  message: AssistantMessage;
39
40
  completed: boolean;
40
41
  cancelled: boolean;
@@ -298,6 +299,7 @@ export class LocalSession {
298
299
  this.#queue.shift();
299
300
  this.#active = {
300
301
  request,
302
+ session: this.#description(),
301
303
  message: output.message,
302
304
  completed: false,
303
305
  cancelled: false,
@@ -342,7 +344,7 @@ export class LocalSession {
342
344
  this.#context = context;
343
345
  const active = this.#active;
344
346
  if (!active || message !== active.message) return;
345
- const sessionId = context.sessionManager.getSessionId();
347
+ const sessionId = active.session.id;
346
348
  for (const result of toolResults) rememberImages(sessionId, result.content);
347
349
  active.completed = true;
348
350
  active.toolResults = toolResults;
@@ -354,44 +356,36 @@ export class LocalSession {
354
356
  this.#collectInputs();
355
357
  const inputs = this.#inputs();
356
358
  if (active.cancelled) {
357
- const context = this.#context;
358
- if (context) {
359
- const sessionFile = context.sessionManager.getSessionFile();
360
- const delivery: DeliveryRecord = {
361
- id: randomUUID(),
362
- chatId: active.request.chatId,
363
- sessionId: context.sessionManager.getSessionId(),
364
- toolCallIds:
365
- active.request.type === "call"
366
- ? active.request.calls.map(({ id }) => id)
367
- : [],
368
- ...(sessionFile
369
- ? { sessionFile }
370
- : { inlineResults: active.toolResults }),
371
- ...(active.message.errorMessage
372
- ? { error: active.message.errorMessage }
373
- : { error: "Request cancelled" }),
374
- };
375
- this.#deliveries.set(delivery.id, delivery);
376
- await this.#flushDeliveries().catch(() => {});
377
- }
359
+ const sessionFile = active.session.sessionFile;
360
+ const delivery: DeliveryRecord = {
361
+ id: randomUUID(),
362
+ chatId: active.request.chatId,
363
+ sessionId: active.session.id,
364
+ cwd: active.session.cwd,
365
+ toolCallIds:
366
+ active.request.type === "call"
367
+ ? active.request.calls.map(({ id }) => id)
368
+ : [],
369
+ ...(sessionFile
370
+ ? { sessionFile }
371
+ : { inlineResults: active.toolResults }),
372
+ ...(active.message.errorMessage
373
+ ? { error: active.message.errorMessage }
374
+ : { error: "Request cancelled" }),
375
+ };
376
+ this.#deliveries.set(delivery.id, delivery);
377
+ await this.#flushDeliveries().catch(() => {});
378
378
  } else {
379
- await this.#connection?.send(
380
- active.request.type === "call"
381
- ? {
382
- type: "result",
383
- id: active.request.id,
384
- message: active.message,
385
- toolResults: active.toolResults,
386
- inputs,
387
- }
388
- : {
389
- type: "result",
390
- id: active.request.id,
391
- message: active.message,
392
- inputs,
393
- },
394
- );
379
+ await this.#connection?.send({
380
+ type: "result",
381
+ id: active.request.id,
382
+ cwd: active.session.cwd,
383
+ message: active.message,
384
+ inputs,
385
+ ...(active.request.type === "call"
386
+ ? { toolResults: active.toolResults }
387
+ : {}),
388
+ });
395
389
  }
396
390
  this.#active = undefined;
397
391
  this.#status = "idle";
package/src/state.ts CHANGED
@@ -40,8 +40,12 @@ export class State {
40
40
  return this.#bindings.get(chatId);
41
41
  }
42
42
 
43
- boundSessions(): Set<string> {
44
- return new Set(this.#bindings.values());
43
+ bindingCounts(): Map<string, number> {
44
+ const counts = new Map<string, number>();
45
+ for (const sessionId of this.#bindings.values()) {
46
+ counts.set(sessionId, (counts.get(sessionId) ?? 0) + 1);
47
+ }
48
+ return counts;
45
49
  }
46
50
 
47
51
  setBinding(chatId: string, sessionId: string): Promise<void> {
package/src/tools.ts CHANGED
@@ -38,10 +38,12 @@ export const directTools = definitions.map((definition) => ({
38
38
  export function toolResult(
39
39
  toolResults: ToolResultMessage[],
40
40
  sessionId: string,
41
+ cwd: string,
41
42
  inputs: SessionInput[] = [],
42
43
  ) {
43
44
  return {
44
45
  content: [
46
+ { type: "text" as const, text: JSON.stringify({ sessionId, cwd }) },
45
47
  ...toolResults.flatMap((result) => [
46
48
  {
47
49
  type: "text" as const,
@@ -70,7 +72,7 @@ export function inputContent(inputs: SessionInput[]) {
70
72
  return inputs.flatMap(({ id, sessionId, message }) => [
71
73
  {
72
74
  type: "text" as const,
73
- text: JSON.stringify({ piInput: id }),
75
+ text: JSON.stringify({ piInput: id, sessionId }),
74
76
  },
75
77
  ...(typeof message.content === "string"
76
78
  ? [{ type: "text" as const, text: message.content }]