pi-sessions 0.2.2 → 0.3.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.
@@ -18,6 +18,7 @@ import { parseTypeBoxValue } from "../shared/typebox.js";
18
18
 
19
19
  const MAX_RELEVANT_FILES = 12;
20
20
  const MAX_OPEN_QUESTIONS = 8;
21
+ const MAX_HANDOFF_TITLE_LENGTH = 64;
21
22
 
22
23
  const HANDOFF_SYSTEM_PROMPT = `You extract context for a deliberate session handoff.
23
24
 
@@ -27,12 +28,16 @@ Rules:
27
28
  - Extract only context that is relevant to the next task.
28
29
  - Keep the summary compact and concrete.
29
30
  - Prefer workspace-relative file paths when possible.
31
+ - title must be a short session title for the new handoff thread, 64 characters or less, without prefixes like "Handoff:" or otherwise referencing the current thread.
30
32
  - nextTask must be the concrete next action for the new session.
31
33
  - openQuestions should contain only unresolved items that materially affect the next task.
32
34
  - If there are no meaningful open questions, omit openQuestions entirely.
33
35
  - Do not write the final handoff prompt yourself.`;
34
36
 
35
37
  const HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
38
+ title: Type.String({
39
+ description: "Short display title for the new handoff session.",
40
+ }),
36
41
  summary: Type.String({
37
42
  description: "Only the context relevant to the next task.",
38
43
  }),
@@ -58,11 +63,13 @@ const HANDOFF_EXTRACTION_TOOL: Tool<typeof HANDOFF_EXTRACTION_PARAMETERS> = {
58
63
  type RequiredHandoffExtractionArgs = Static<typeof REQUIRED_HANDOFF_EXTRACTION_PARAMETERS>;
59
64
 
60
65
  const REQUIRED_HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
66
+ title: HANDOFF_EXTRACTION_PARAMETERS.properties.title,
61
67
  summary: HANDOFF_EXTRACTION_PARAMETERS.properties.summary,
62
68
  nextTask: HANDOFF_EXTRACTION_PARAMETERS.properties.nextTask,
63
69
  });
64
70
 
65
71
  export interface HandoffContext {
72
+ title: string;
66
73
  summary: string;
67
74
  relevantFiles: string[];
68
75
  nextTask: string;
@@ -139,11 +146,13 @@ export async function generateHandoffDraft(
139
146
  throw new Error(response.errorMessage ?? "Handoff generation failed.");
140
147
  }
141
148
 
142
- const handoffContext = extractHandoffContext(response, goal);
143
- if (!handoffContext) {
144
- throw new Error("Handoff extraction did not return structured context.");
149
+ const extraction = extractHandoffContext(response, goal);
150
+ if (!extraction.context) {
151
+ throw new Error(extraction.error);
145
152
  }
146
153
 
154
+ const handoffContext = extraction.context;
155
+
147
156
  const sessionId = ctx.sessionManager.getSessionId();
148
157
  const sessionPath = ctx.sessionManager.getSessionFile();
149
158
 
@@ -208,10 +217,10 @@ export function assembleHandoffDraft(
208
217
  export function extractHandoffContext(
209
218
  response: AssistantMessage,
210
219
  goal: string,
211
- ): HandoffContext | undefined {
220
+ ): { context: HandoffContext; error?: undefined } | { context?: undefined; error: string } {
212
221
  const toolCall = response.content.find(isCreateHandoffContextToolCall);
213
222
  if (!toolCall) {
214
- return undefined;
223
+ return { error: "Handoff extraction did not return structured context." };
215
224
  }
216
225
 
217
226
  let requiredArguments: RequiredHandoffExtractionArgs;
@@ -222,7 +231,12 @@ export function extractHandoffContext(
222
231
  "Invalid create_handoff_context arguments",
223
232
  );
224
233
  } catch {
225
- return undefined;
234
+ return { error: "Handoff extraction did not return structured context." };
235
+ }
236
+
237
+ const title = normalizeText(requiredArguments.title);
238
+ if (title.length > MAX_HANDOFF_TITLE_LENGTH) {
239
+ return { error: "Handoff title must be 64 characters or less." };
226
240
  }
227
241
 
228
242
  const summary = normalizeText(requiredArguments.summary);
@@ -230,15 +244,18 @@ export function extractHandoffContext(
230
244
  const nextTask = normalizeText(requiredArguments.nextTask) || goal.trim();
231
245
  const openQuestions = normalizeStringArray(toolCall.arguments.openQuestions, MAX_OPEN_QUESTIONS);
232
246
 
233
- if (!summary || !nextTask) {
234
- return undefined;
247
+ if (!summary || !nextTask || !title) {
248
+ return { error: "Handoff extraction did not return structured context." };
235
249
  }
236
250
 
237
251
  return {
238
- summary,
239
- relevantFiles,
240
- nextTask,
241
- openQuestions,
252
+ context: {
253
+ title,
254
+ summary,
255
+ relevantFiles,
256
+ nextTask,
257
+ openQuestions,
258
+ },
242
259
  };
243
260
  }
244
261
 
@@ -12,6 +12,7 @@ export const HANDOFF_SESSION_METADATA_SCHEMA = Type.Object({
12
12
  origin: Type.Literal("handoff"),
13
13
  goal: Type.String(),
14
14
  nextTask: Type.String(),
15
+ title: Type.String(),
15
16
  initial_prompt: Type.String(),
16
17
  });
17
18
 
@@ -19,6 +20,7 @@ export const HANDOFF_BOOTSTRAP_SCHEMA = Type.Object({
19
20
  sessionId: Type.String(),
20
21
  goal: Type.String(),
21
22
  nextTask: Type.String(),
23
+ title: Type.String(),
22
24
  initialPrompt: Type.String(),
23
25
  });
24
26
 
@@ -29,6 +31,7 @@ export function createHandoffSessionMetadata(
29
31
  goal: string,
30
32
  nextTask: string,
31
33
  initialPrompt: string,
34
+ title: string,
32
35
  ): HandoffSessionMetadata {
33
36
  const normalizedGoal = goal.trim();
34
37
  const normalizedNextTask = nextTask.trim() || normalizedGoal;
@@ -37,6 +40,7 @@ export function createHandoffSessionMetadata(
37
40
  origin: "handoff",
38
41
  goal: normalizedGoal,
39
42
  nextTask: normalizedNextTask,
43
+ title,
40
44
  initial_prompt: initialPrompt.trim(),
41
45
  };
42
46
  }
@@ -49,6 +53,7 @@ export function createHandoffBootstrap(
49
53
  sessionId,
50
54
  goal: metadata.goal,
51
55
  nextTask: metadata.nextTask,
56
+ title: metadata.title,
52
57
  initialPrompt: metadata.initial_prompt,
53
58
  };
54
59
  }
@@ -6,6 +6,7 @@ import {
6
6
  type ExtensionAPI,
7
7
  type ExtensionCommandContext,
8
8
  type SessionHeader,
9
+ type SessionInfoEntry,
9
10
  } from "@earendil-works/pi-coding-agent";
10
11
  import { HANDOFF_BOOTSTRAP_ENV } from "./metadata.js";
11
12
 
@@ -44,6 +45,7 @@ export function createHandoffSession(options: {
44
45
  cwd: string;
45
46
  sessionDir: string;
46
47
  parentSessionFile: string;
48
+ title: string;
47
49
  }): CreatedHandoffSession {
48
50
  const sessionId = randomUUID();
49
51
  const timestamp = new Date().toISOString();
@@ -59,7 +61,18 @@ export function createHandoffSession(options: {
59
61
  parentSession: options.parentSessionFile,
60
62
  };
61
63
 
62
- writeFileSync(sessionFile, `${JSON.stringify(header)}\n`);
64
+ const titleEntry: SessionInfoEntry = {
65
+ type: "session_info",
66
+ id: randomUUID(),
67
+ parentId: null,
68
+ timestamp,
69
+ name: options.title,
70
+ };
71
+
72
+ writeFileSync(
73
+ sessionFile,
74
+ `${[JSON.stringify(header), JSON.stringify(titleEntry)].join("\n")}\n`,
75
+ );
63
76
 
64
77
  return { sessionId, sessionFile };
65
78
  }
@@ -72,12 +85,14 @@ export async function launchSplitHandoffSession(
72
85
  direction: HandoffSplitDirection;
73
86
  sessionId: string;
74
87
  bootstrapValue: string;
88
+ title: string;
75
89
  },
76
90
  ): Promise<{ success: true } | { success: false; error: string }> {
77
91
  const piCommand = buildPiLaunchCommand(
78
92
  options.sessionDir,
79
93
  options.sessionId,
80
94
  options.bootstrapValue,
95
+ options.title,
81
96
  );
82
97
  const escapedCwd = escapeAppleScriptString(options.cwd);
83
98
  const escapedCommand = escapeAppleScriptString(piCommand);
@@ -113,23 +128,29 @@ export function buildPiResumeCommand(
113
128
  sessionDir: string,
114
129
  sessionId: string,
115
130
  bootstrapValue: string,
131
+ title: string,
116
132
  ): string {
117
- return [
133
+ const args = [
118
134
  `${HANDOFF_BOOTSTRAP_ENV}=${shellQuote(bootstrapValue)}`,
119
135
  "pi",
120
136
  "--session-dir",
121
137
  shellQuote(sessionDir),
122
- "--session",
138
+ "--session-id",
123
139
  shellQuote(sessionId),
124
- ].join(" ");
140
+ "--name",
141
+ shellQuote(title),
142
+ ];
143
+
144
+ return args.join(" ");
125
145
  }
126
146
 
127
147
  export function buildPiLaunchCommand(
128
148
  sessionDir: string,
129
149
  sessionId: string,
130
150
  bootstrapValue: string,
151
+ title: string,
131
152
  ): string {
132
- const payload = `${buildPiResumeCommand(sessionDir, sessionId, bootstrapValue)}; exec /bin/zsh -il`;
153
+ const payload = `${buildPiResumeCommand(sessionDir, sessionId, bootstrapValue, title)}; exec /bin/zsh -il`;
133
154
  return `/bin/zsh -ilc ${shellQuote(payload)}`;
134
155
  }
135
156
 
@@ -23,17 +23,23 @@ import {
23
23
  launchSplitHandoffSession,
24
24
  validateSplitHandoffPrerequisites,
25
25
  } from "./session-handoff/spawn.js";
26
+ import { isTuiMode } from "./shared/pi-mode.js";
26
27
  import { loadSettings } from "./shared/settings.js";
27
28
 
28
29
  const HANDOFF_USAGE = "Usage: /handoff [--left|--right|--up|--down] <goal for new thread>";
29
30
 
31
+ interface HandoffPromptContext {
32
+ ui: ExtensionCommandContext["ui"];
33
+ sendUserMessage(content: string): Promise<void>;
34
+ }
35
+
30
36
  export default function sessionHandoffExtension(pi: ExtensionAPI): void {
31
37
  const settings = loadSettings();
32
38
 
33
39
  pi.registerCommand("handoff", {
34
40
  description: "Transfer context to a new focused session",
35
41
  handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
36
- if (!ctx.hasUI) {
42
+ if (!isTuiMode(ctx)) {
37
43
  ctx.ui.notify("handoff requires interactive mode", "error");
38
44
  return;
39
45
  }
@@ -99,28 +105,30 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
99
105
  parsedArgs.goal,
100
106
  generatedDraft.context.nextTask,
101
107
  approvedDraft,
108
+ generatedDraft.context.title,
102
109
  );
103
- const createdSession = createHandoffSession({
104
- cwd: ctx.cwd,
105
- sessionDir: ctx.sessionManager.getSessionDir(),
106
- parentSessionFile,
107
- });
108
- const bootstrapValue = encodeHandoffBootstrap(
109
- createHandoffBootstrap(createdSession.sessionId, handoffMetadata),
110
- );
111
-
112
110
  if (parsedArgs.splitDirection) {
111
+ const createdSession = createHandoffSession({
112
+ cwd: ctx.cwd,
113
+ sessionDir: ctx.sessionManager.getSessionDir(),
114
+ parentSessionFile,
115
+ title: handoffMetadata.title,
116
+ });
117
+ const bootstrapValue = encodeHandoffBootstrap(
118
+ createHandoffBootstrap(createdSession.sessionId, handoffMetadata),
119
+ );
113
120
  const launchResult = await launchSplitHandoffSession(pi, {
114
121
  cwd: ctx.cwd,
115
122
  sessionDir: ctx.sessionManager.getSessionDir(),
116
123
  direction: parsedArgs.splitDirection,
117
124
  sessionId: createdSession.sessionId,
118
125
  bootstrapValue,
126
+ title: handoffMetadata.title,
119
127
  });
120
128
 
121
129
  if (!launchResult.success) {
122
130
  ctx.ui.notify(
123
- `${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(ctx.sessionManager.getSessionDir(), createdSession.sessionId, bootstrapValue)}`,
131
+ `${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(ctx.sessionManager.getSessionDir(), createdSession.sessionId, bootstrapValue, handoffMetadata.title)}`,
124
132
  "error",
125
133
  );
126
134
  return;
@@ -130,10 +138,14 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
130
138
  return;
131
139
  }
132
140
 
133
- const switchResult = await ctx.switchSession(createdSession.sessionFile, {
141
+ const switchResult = await ctx.newSession({
142
+ parentSession: parentSessionFile,
143
+ setup: async (sessionManager) => {
144
+ sessionManager.appendSessionInfo(handoffMetadata.title);
145
+ sessionManager.appendCustomEntry(HANDOFF_METADATA_CUSTOM_TYPE, handoffMetadata);
146
+ },
134
147
  withSession: async (nextCtx) => {
135
- await nextCtx.sendUserMessage(approvedDraft);
136
- nextCtx.ui.notify("Handoff started in a new session.", "info");
148
+ startHandoffPromptAfterSessionRender(nextCtx, approvedDraft);
137
149
  },
138
150
  });
139
151
 
@@ -146,7 +158,7 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
146
158
  pi.registerShortcut(settings.handoff.pickerShortcut, {
147
159
  description: "Open the session reference picker",
148
160
  handler: async (ctx) => {
149
- if (!ctx.hasUI) {
161
+ if (!isTuiMode(ctx)) {
150
162
  return;
151
163
  }
152
164
 
@@ -191,10 +203,19 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
191
203
  if (!getHandoffMetadataFromEntries(entries)) {
192
204
  pi.appendEntry(
193
205
  HANDOFF_METADATA_CUSTOM_TYPE,
194
- createHandoffSessionMetadata(bootstrap.goal, bootstrap.nextTask, bootstrap.initialPrompt),
206
+ createHandoffSessionMetadata(
207
+ bootstrap.goal,
208
+ bootstrap.nextTask,
209
+ bootstrap.initialPrompt,
210
+ bootstrap.title,
211
+ ),
195
212
  );
196
213
  }
197
214
 
215
+ if (!ctx.sessionManager.getSessionName()) {
216
+ pi.setSessionName(bootstrap.title);
217
+ }
218
+
198
219
  pi.sendUserMessage(bootstrap.initialPrompt);
199
220
  } finally {
200
221
  delete process.env[HANDOFF_BOOTSTRAP_ENV];
@@ -266,6 +287,23 @@ async function runWithLoader<T>(
266
287
  return result;
267
288
  }
268
289
 
290
+ function startHandoffPromptAfterSessionRender(
291
+ ctx: HandoffPromptContext,
292
+ approvedDraft: string,
293
+ ): void {
294
+ // ctx.newSession() renders the replacement session only after withSession returns.
295
+ setImmediate(() => {
296
+ void (async () => {
297
+ try {
298
+ await ctx.sendUserMessage(approvedDraft);
299
+ ctx.ui.notify("Handoff started in a new session.", "info");
300
+ } catch (error) {
301
+ ctx.ui.notify(formatHandoffError(error), "error");
302
+ }
303
+ })();
304
+ });
305
+ }
306
+
269
307
  function formatHandoffError(error: unknown): string {
270
308
  if (error instanceof Error && error.message.trim()) {
271
309
  return error.message;
@@ -6,6 +6,7 @@ import {
6
6
  } from "@earendil-works/pi-coding-agent";
7
7
  import { type Focusable, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
8
8
  import { type ReindexResult, rebuildSessionIndex } from "./session-search/reindex.js";
9
+ import { isTuiMode } from "./shared/pi-mode.js";
9
10
  import { getIndexStatus, type SessionIndexStatus } from "./shared/session-index/index.js";
10
11
  import { loadSettings } from "./shared/settings.js";
11
12
 
@@ -17,7 +18,7 @@ export default function sessionIndexExtension(pi: ExtensionAPI): void {
17
18
  pi.registerCommand("session-index", {
18
19
  description: "Open the session index control panel",
19
20
  handler: async (_args, ctx) => {
20
- if (!ctx.hasUI) {
21
+ if (!isTuiMode(ctx)) {
21
22
  ctx.ui.notify("/session-index requires interactive mode.", "warning");
22
23
  return;
23
24
  }
@@ -0,0 +1,5 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export function isTuiMode(ctx: Pick<ExtensionContext, "mode">): boolean {
4
+ return ctx.mode === "tui";
5
+ }
@@ -6,6 +6,7 @@ import { type Static, Type } from "typebox";
6
6
  import { parseTypeBoxValue } from "./typebox.js";
7
7
 
8
8
  export const DEFAULT_AUTO_TITLE_REFRESH_TURNS = 4;
9
+ export const DEFAULT_AUTO_TITLE_PROMPT = `Name this coding session (under 80 chars). Be specific to what is being discussed. Your exact output will be displayed to the user, so make sure that it contains ONLY the title itself and nothing else.`;
9
10
  const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
10
11
  handoff: Type.Optional(
11
12
  Type.Object({
@@ -21,6 +22,7 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
21
22
  Type.Object({
22
23
  refreshTurns: Type.Optional(Type.Integer({ minimum: 1 })),
23
24
  model: Type.Optional(Type.String()),
25
+ prompt: Type.Optional(Type.String()),
24
26
  }),
25
27
  ),
26
28
  });
@@ -42,6 +44,7 @@ export class ModelReference {
42
44
  export interface AutoTitleSettings {
43
45
  refreshTurns: number;
44
46
  model: ModelReference | undefined;
47
+ prompt: string;
45
48
  }
46
49
 
47
50
  export interface SessionSettings {
@@ -113,6 +116,11 @@ function parseModelReference(value: string | undefined): ModelReference | undefi
113
116
  return new ModelReference(trimmed.slice(0, slashIndex), trimmed.slice(slashIndex + 1));
114
117
  }
115
118
 
119
+ function normalizeAutoTitlePrompt(value: string | undefined): string {
120
+ const trimmed = value?.trim();
121
+ return trimmed ? trimmed : DEFAULT_AUTO_TITLE_PROMPT;
122
+ }
123
+
116
124
  function loadSessionFileSettings(): SessionFileSettings {
117
125
  const globalSettings = SettingsManager.create(process.cwd()).getGlobalSettings();
118
126
  const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA, globalSettings, "Invalid settings");
@@ -132,6 +140,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
132
140
  autoTitle: {
133
141
  refreshTurns: fileSettings.autoTitle?.refreshTurns ?? DEFAULT_AUTO_TITLE_REFRESH_TURNS,
134
142
  model: parseModelReference(fileSettings.autoTitle?.model),
143
+ prompt: normalizeAutoTitlePrompt(fileSettings.autoTitle?.prompt),
135
144
  },
136
145
  };
137
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sessions",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
6
6
  "license": "MIT",
@@ -42,10 +42,10 @@
42
42
  },
43
43
  "devDependencies": {
44
44
  "@biomejs/biome": "^2.4.14",
45
- "@earendil-works/pi-agent-core": "^0.74.0",
46
- "@earendil-works/pi-ai": "^0.74.0",
47
- "@earendil-works/pi-coding-agent": "^0.74.0",
48
- "@earendil-works/pi-tui": "^0.74.0",
45
+ "@earendil-works/pi-agent-core": "^0.78.1",
46
+ "@earendil-works/pi-ai": "^0.78.1",
47
+ "@earendil-works/pi-coding-agent": "^0.78.1",
48
+ "@earendil-works/pi-tui": "^0.78.1",
49
49
  "@types/better-sqlite3": "^7.6.13",
50
50
  "@types/node": "^25.6.2",
51
51
  "husky": "^9.1.7",
@@ -55,10 +55,10 @@
55
55
  "vitest": "^4.1.5"
56
56
  },
57
57
  "peerDependencies": {
58
- "@earendil-works/pi-agent-core": ">=0.74.0",
59
- "@earendil-works/pi-ai": ">=0.74.0",
60
- "@earendil-works/pi-coding-agent": ">=0.74.0",
61
- "@earendil-works/pi-tui": ">=0.74.0",
58
+ "@earendil-works/pi-agent-core": ">=0.78.1",
59
+ "@earendil-works/pi-ai": ">=0.78.1",
60
+ "@earendil-works/pi-coding-agent": ">=0.78.1",
61
+ "@earendil-works/pi-tui": ">=0.78.1",
62
62
  "typebox": ">=1.1.24"
63
63
  },
64
64
  "lint-staged": {
@@ -1,54 +0,0 @@
1
- import type { AutoTitleContext } from "./context.js";
2
- import type { AutoTitleTrigger } from "./state.js";
3
-
4
- const AUTO_TITLE_CHAR_MAX = 120;
5
-
6
- export const AUTO_TITLE_SYSTEM_PROMPT = `You generate concrete coding session titles from the conversation provided.
7
-
8
- Return title text only.
9
-
10
- Rules:
11
- - Prefer 3-15 words.
12
- - Maximum 120 characters.
13
- - Use the full conversation, while letting the most recent work refine the title when needed.
14
- - Describe the current task, bug, feature, or investigation on the active branch.
15
- - Mention specific subsystem or file only when it improves clarity.
16
- - No quotes, markdown, emojis, prefixes, or explanations.
17
- - No trailing punctuation.
18
- - Avoid generic titles like Coding help or Working on project.`;
19
-
20
- export function buildAutoTitlePrompt(context: AutoTitleContext, trigger: AutoTitleTrigger): string {
21
- const sections = [
22
- ["## Trigger", trigger],
23
- ["## Current Title", context.currentTitle ?? "(none)"],
24
- ["## Counts", formatCounts(context)],
25
- ["## Conversation", context.conversationText || "(none)"],
26
- ];
27
-
28
- if (context.cwd) {
29
- sections.unshift(["## Cwd", context.cwd]);
30
- }
31
-
32
- return sections.map(([heading, body]) => `${heading}\n${body}`).join("\n\n");
33
- }
34
-
35
- export function normalizeGeneratedAutoTitle(value: string): string | undefined {
36
- const withoutQuotes = value.trim().replace(/^["'`]+|["'`]+$/g, "");
37
- const collapsed = withoutQuotes
38
- .replace(/\s+/g, " ")
39
- .replace(/[.!?]+$/g, "")
40
- .trim();
41
- if (!collapsed) {
42
- return undefined;
43
- }
44
-
45
- const truncated = collapsed.slice(0, AUTO_TITLE_CHAR_MAX).trim();
46
- return truncated || undefined;
47
- }
48
-
49
- function formatCounts(context: AutoTitleContext): string {
50
- return [
51
- `user_turns: ${context.userTurnCount}`,
52
- `assistant_turns: ${context.assistantTurnCount}`,
53
- ].join("\n");
54
- }