@llblab/pi-actors 0.40.1 → 0.41.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.
@@ -16,11 +16,13 @@ export function createRunUiObservationState() {
16
16
  outboxEventIds: new Map(),
17
17
  };
18
18
  }
19
- export function readRunUiSnapshot(state, ownerId) {
20
- const summary = summarizeRuns(undefined, ownerId);
19
+ export function readRunUiSnapshot(state, ownerId, options = {}) {
20
+ const summary = summarizeRuns(options.stateRoot, ownerId);
21
21
  const status = renderRunStatus(summary, state.frame++);
22
22
  return {
23
- outboxEvents: detectRunOutboxEvents(state.eventLines, summary, state.outboxEventIds),
23
+ outboxEvents: options.includeOutbox === false
24
+ ? []
25
+ : detectRunOutboxEvents(state.eventLines, summary, state.outboxEventIds),
24
26
  status,
25
27
  summary,
26
28
  transitions: detectRunTransitions(state.observed, summary),
@@ -29,25 +31,43 @@ export function readRunUiSnapshot(state, ownerId) {
29
31
  export function pruneRunUiObservationState(state, snapshot) {
30
32
  pruneRunObservationState(state.observed, state.eventLines, snapshot.summary, snapshot.transitions.map((transition) => transition.stateDir ?? transition.run), state.outboxEventIds);
31
33
  }
32
- export function deliverRunTransitionNotifications(transitions, sink) {
34
+ export function deliverRunTransitionNotifications(transitions, sink, inFlight = new Set()) {
33
35
  for (const transition of transitions) {
34
36
  if (!shouldNotifyRunTransition(transition))
35
37
  continue;
36
- const text = formatRunTransitionMessage(transition);
37
- sink.notify(text, getRunTransitionNotificationType(transition));
38
- if (!shouldSendRunTransitionFollowUp(transition))
38
+ const key = transition.stateDir ?? transition.run;
39
+ if (inFlight.has(key))
39
40
  continue;
40
- sink.sendFollowUp({
41
- customType: "pi-actors-run",
42
- content: text,
43
- display: true,
44
- details: transition,
45
- });
46
- if (transition.stateDir) {
47
- AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
41
+ inFlight.add(key);
42
+ try {
43
+ const text = formatRunTransitionMessage(transition);
44
+ sink.notify(text, getRunTransitionNotificationType(transition));
45
+ if (!shouldSendRunTransitionFollowUp(transition))
46
+ continue;
47
+ sink.sendFollowUp({
48
+ customType: "pi-actors-run",
49
+ content: text,
50
+ display: true,
51
+ details: transition,
52
+ });
53
+ if (transition.stateDir) {
54
+ AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
55
+ }
56
+ }
57
+ finally {
58
+ inFlight.delete(key);
48
59
  }
49
60
  }
50
61
  }
62
+ export function reconcileRunTerminalNotifications(input) {
63
+ const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
64
+ includeOutbox: false,
65
+ stateRoot: input.stateRoot,
66
+ });
67
+ deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
68
+ pruneRunUiObservationState(input.state, snapshot);
69
+ return snapshot;
70
+ }
51
71
  export function deliverRunOutboxNotifications(events, sink) {
52
72
  for (const event of events) {
53
73
  if (!shouldNotifyRunOutboxEvent(event))
@@ -64,10 +84,58 @@ export function deliverRunOutboxNotifications(events, sink) {
64
84
  });
65
85
  }
66
86
  }
87
+ const RUN_WATCHER_DIAGNOSTIC_LIMIT = 32;
67
88
  export function createRunStateWatcher(input) {
68
89
  const stateRoot = input.stateRoot ?? Paths.getRunStateRoot();
90
+ const pathExists = input.exists ?? existsSync;
91
+ const listDirectories = input.listDirectories ??
92
+ ((path) => readdirSync(path, { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory())
94
+ .map((entry) => join(path, entry.name)));
95
+ const watchPath = input.watchPath ?? ((path, onChange) => watch(path, onChange));
96
+ let diagnosticId = 0;
97
+ let rootDegraded = false;
69
98
  let stateRootWatcher;
99
+ const degradedRunDirs = new Set();
100
+ const diagnostics = [];
101
+ const lastDiagnosticSignatures = new Map();
70
102
  const runDirWatchers = new Map();
103
+ const record = (code, scope, path, error) => {
104
+ const detail = error instanceof Error ? `: ${error.message}` : "";
105
+ const signature = `${code}${detail}`;
106
+ const signatureKey = `${scope}:${path}`;
107
+ if (lastDiagnosticSignatures.get(signatureKey) === signature)
108
+ return;
109
+ lastDiagnosticSignatures.set(signatureKey, signature);
110
+ const recovery = code === "rearmed"
111
+ ? "; terminal watch acceleration restored"
112
+ : "; terminal reconciliation remains active and watcher rearm will retry";
113
+ diagnostics.push({
114
+ code,
115
+ id: ++diagnosticId,
116
+ message: `Run-state ${scope} watcher ${code.replace("_", " ")} for ${path}${detail}${recovery}`,
117
+ path,
118
+ scope,
119
+ ts: new Date().toISOString(),
120
+ });
121
+ if (diagnostics.length > RUN_WATCHER_DIAGNOSTIC_LIMIT)
122
+ diagnostics.shift();
123
+ };
124
+ const removeRunWatcher = (stateDir, watcher, options = {}) => {
125
+ if (runDirWatchers.get(stateDir) !== watcher)
126
+ return;
127
+ watcher.close();
128
+ runDirWatchers.delete(stateDir);
129
+ if (options.degraded === false) {
130
+ degradedRunDirs.delete(stateDir);
131
+ lastDiagnosticSignatures.delete(`run:${stateDir}`);
132
+ return;
133
+ }
134
+ degradedRunDirs.add(stateDir);
135
+ if (options.error)
136
+ record("error", "run", stateDir, options.error);
137
+ record("removed", "run", stateDir);
138
+ };
71
139
  const close = () => {
72
140
  stateRootWatcher?.close();
73
141
  stateRootWatcher = undefined;
@@ -76,42 +144,92 @@ export function createRunStateWatcher(input) {
76
144
  runDirWatchers.clear();
77
145
  };
78
146
  const watchRunDir = (stateDir) => {
79
- if (runDirWatchers.has(stateDir) || !existsSync(stateDir))
147
+ if (runDirWatchers.has(stateDir) || !pathExists(stateDir))
80
148
  return;
81
149
  try {
82
- const watcher = watch(stateDir, input.onChange);
83
- watcher.on("error", () => {
84
- watcher.close();
85
- runDirWatchers.delete(stateDir);
86
- });
150
+ const watcher = watchPath(stateDir, input.onChange);
151
+ watcher.on("error", (error) => removeRunWatcher(stateDir, watcher, { error }));
87
152
  runDirWatchers.set(stateDir, watcher);
153
+ if (degradedRunDirs.delete(stateDir))
154
+ record("rearmed", "run", stateDir);
88
155
  }
89
- catch {
90
- // Watching is best-effort; explicit inspect remains available.
156
+ catch (error) {
157
+ degradedRunDirs.add(stateDir);
158
+ record("attach_failed", "run", stateDir, error);
91
159
  }
92
160
  };
93
161
  function refresh() {
94
- if (!existsSync(stateRoot))
162
+ if (!pathExists(stateRoot))
95
163
  return;
96
164
  if (!stateRootWatcher) {
97
165
  try {
98
- stateRootWatcher = watch(stateRoot, input.onChange);
99
- stateRootWatcher.on("error", () => {
100
- stateRootWatcher?.close();
166
+ const watcher = watchPath(stateRoot, input.onChange);
167
+ stateRootWatcher = watcher;
168
+ watcher.on("error", (error) => {
169
+ if (stateRootWatcher !== watcher)
170
+ return;
171
+ watcher.close();
101
172
  stateRootWatcher = undefined;
173
+ rootDegraded = true;
174
+ record("error", "root", stateRoot, error);
175
+ record("removed", "root", stateRoot);
102
176
  });
177
+ if (rootDegraded) {
178
+ rootDegraded = false;
179
+ record("rearmed", "root", stateRoot);
180
+ }
103
181
  }
104
- catch {
105
- // Watching is best-effort; explicit inspect remains available.
182
+ catch (error) {
183
+ rootDegraded = true;
184
+ record("attach_failed", "root", stateRoot, error);
106
185
  }
107
186
  }
108
- for (const entry of readdirSync(stateRoot, { withFileTypes: true })) {
109
- if (!entry.isDirectory())
187
+ let stateDirs;
188
+ try {
189
+ stateDirs = listDirectories(stateRoot);
190
+ }
191
+ catch (error) {
192
+ record("attach_failed", "root", stateRoot, error);
193
+ return;
194
+ }
195
+ const present = new Set(stateDirs);
196
+ for (const [stateDir, watcher] of runDirWatchers) {
197
+ if (present.has(stateDir) && pathExists(stateDir))
110
198
  continue;
111
- watchRunDir(`${stateRoot}/${entry.name}`);
199
+ removeRunWatcher(stateDir, watcher, { degraded: false });
112
200
  }
201
+ for (const stateDir of stateDirs)
202
+ watchRunDir(stateDir);
113
203
  }
114
- return { close, refresh };
204
+ return {
205
+ close,
206
+ getDiagnostics: () => [...diagnostics],
207
+ refresh,
208
+ };
209
+ }
210
+ export function createRunTerminalReconciliationLoop(input) {
211
+ const intervalMs = input.intervalMs ?? 10_000;
212
+ let interval;
213
+ const reconcileNow = () => {
214
+ try {
215
+ input.refreshWatcher();
216
+ input.reconcile();
217
+ }
218
+ catch (error) {
219
+ input.onError?.(error);
220
+ }
221
+ };
222
+ const close = () => {
223
+ if (interval)
224
+ clearInterval(interval);
225
+ interval = undefined;
226
+ };
227
+ const start = () => {
228
+ close();
229
+ interval = setInterval(reconcileNow, intervalMs);
230
+ interval.unref?.();
231
+ };
232
+ return { close, reconcileNow, start };
115
233
  }
116
234
  const TERMINAL = new Set([
117
235
  "done",
@@ -14,6 +14,11 @@ export declare function markRecipeContextRecords(records: TemplateRecipeContextR
14
14
  export declare function formatRecipeContextJsonl(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
15
15
  export declare function buildRecipeContextPromptBlock(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
16
16
  export declare function appendRecipeContextToPiArgs(command: string, args: string[], records: TemplateRecipeContextRecord[] | undefined, context?: CommandTemplateActorRecipeContext): string[];
17
+ export interface ManagedPiSessionArgs {
18
+ args: string[];
19
+ sessionDir?: string;
20
+ }
21
+ export declare function attachPiSessionDir(command: string, args: string[], sessionDir: string): ManagedPiSessionArgs;
17
22
  export interface MaterializedPiPrintPromptArgs {
18
23
  args: string[];
19
24
  promptBytes?: number;
@@ -134,6 +134,21 @@ export function appendRecipeContextToPiArgs(command, args, records, context) {
134
134
  next[promptIndex] = `${next[promptIndex]}\n\n${block}`;
135
135
  return next;
136
136
  }
137
+ const PI_SESSION_POLICY_FLAGS = new Set([
138
+ "--fork",
139
+ "--no-session",
140
+ "--session",
141
+ "--session-dir",
142
+ "--session-id",
143
+ ]);
144
+ export function attachPiSessionDir(command, args, sessionDir) {
145
+ if (!isPiCommand(command) || findPiPrintPromptIndex(args) === undefined) {
146
+ return { args };
147
+ }
148
+ if (args.some((arg) => PI_SESSION_POLICY_FLAGS.has(arg)))
149
+ return { args };
150
+ return { args: ["--session-dir", sessionDir, ...args], sessionDir };
151
+ }
137
152
  export function materializePiPrintPromptArg(command, args, promptFile) {
138
153
  if (!isPiCommand(command))
139
154
  return { args };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Persisted Pi session evidence reader.
3
+ * Zones: subagent turns, active session branches, bounded/redacted previews
4
+ * Owns read-only normalization of session JSONL into inspector-ready turns.
5
+ */
6
+ import { type StateReadDiagnostic } from "./state-readers.ts";
7
+ export interface SessionEvidenceToolCall {
8
+ arguments?: unknown;
9
+ id: string;
10
+ name: string;
11
+ result?: unknown;
12
+ resultError?: boolean;
13
+ }
14
+ export interface SessionEvidenceTurn {
15
+ assistantEntryId?: string;
16
+ assistantText?: string;
17
+ error?: string;
18
+ index: number;
19
+ model?: string;
20
+ provider?: string;
21
+ stopReason?: string;
22
+ thinking?: string;
23
+ timestamp?: string;
24
+ toolCalls: SessionEvidenceToolCall[];
25
+ unmatchedToolResults: number;
26
+ usage?: unknown;
27
+ userEntryId?: string;
28
+ userText?: string;
29
+ }
30
+ export interface SessionEvidence {
31
+ activeLeafId?: string;
32
+ diagnostics: StateReadDiagnostic[];
33
+ path: string;
34
+ session?: Record<string, unknown>;
35
+ totalTurns: number;
36
+ truncated: boolean;
37
+ turns: SessionEvidenceTurn[];
38
+ }
39
+ export interface SessionEvidenceReadOptions {
40
+ maxTextChars?: number;
41
+ maxToolCalls?: number;
42
+ maxTurns?: number;
43
+ }
44
+ export declare function redactSessionEvidenceValue(value: unknown, maxTextChars?: number, seen?: WeakSet<object>): unknown;
45
+ export declare function readSessionEvidence(path: string, options?: SessionEvidenceReadOptions): SessionEvidence;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Persisted Pi session evidence reader.
3
+ * Zones: subagent turns, active session branches, bounded/redacted previews
4
+ * Owns read-only normalization of session JSONL into inspector-ready turns.
5
+ */
6
+ import * as Limits from "./limits.js";
7
+ import { readJsonlFileResilient, } from "./state-readers.js";
8
+ const SENSITIVE_KEY = /(?:^|[_-])(?:api[-_]?key|authorization|cookie|credential|password|private[-_]?key|secret|secret[-_]?access[-_]?key|token)$|^(?:access|refresh|auth|api)Token$|^(?:clientSecret|privateKey|secretAccessKey)$/i;
9
+ const SENSITIVE_TEXT = /(bearer\s+)[A-Za-z0-9._~+/=-]+|["']?\b(api[-_]?key|authorization|clientSecret|cookie|password|private[-_]?key|privateKey|secret|secretAccessKey|token)["']?(\s*[:=]\s*)["']?([^\s,;"'}]+)/gi;
10
+ function asRecord(value) {
11
+ return value && typeof value === "object" && !Array.isArray(value)
12
+ ? value
13
+ : {};
14
+ }
15
+ function boundedText(value, maxChars) {
16
+ if (typeof value !== "string")
17
+ return undefined;
18
+ if (/^\s*[\[{]/.test(value)) {
19
+ try {
20
+ const structured = JSON.stringify(redactSessionEvidenceValue(JSON.parse(value), maxChars));
21
+ return structured.length > maxChars
22
+ ? `${structured.slice(0, Math.max(0, maxChars - 1))}…`
23
+ : structured;
24
+ }
25
+ catch {
26
+ // Fall through to bounded text redaction.
27
+ }
28
+ }
29
+ const redacted = value.replaceAll(SENSITIVE_TEXT, (match, bearer, key, separator) => bearer ? `${bearer}[REDACTED]` : `${key}${separator}[REDACTED]`);
30
+ return redacted.length > maxChars
31
+ ? `${redacted.slice(0, Math.max(0, maxChars - 1))}…`
32
+ : redacted;
33
+ }
34
+ export function redactSessionEvidenceValue(value, maxTextChars = Limits.SESSION_EVIDENCE_TEXT_CHARS, seen = new WeakSet()) {
35
+ if (typeof value === "string")
36
+ return boundedText(value, maxTextChars);
37
+ if (!value || typeof value !== "object")
38
+ return value;
39
+ if (seen.has(value))
40
+ return "[CIRCULAR]";
41
+ seen.add(value);
42
+ if (Array.isArray(value)) {
43
+ return value.map((item) => redactSessionEvidenceValue(item, maxTextChars, seen));
44
+ }
45
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
46
+ key,
47
+ SENSITIVE_KEY.test(key)
48
+ ? "[REDACTED]"
49
+ : redactSessionEvidenceValue(item, maxTextChars, seen),
50
+ ]));
51
+ }
52
+ function contentBlocks(message) {
53
+ return Array.isArray(message.content)
54
+ ? message.content.map(asRecord)
55
+ : typeof message.content === "string"
56
+ ? [{ type: "text", text: message.content }]
57
+ : [];
58
+ }
59
+ function contentText(message, type, maxChars) {
60
+ const text = contentBlocks(message)
61
+ .filter((block) => block.type === type && typeof block[type] === "string")
62
+ .map((block) => String(block[type]))
63
+ .join("\n");
64
+ return boundedText(text, maxChars);
65
+ }
66
+ function activeBranch(entries, path, diagnostics) {
67
+ const treeEntries = entries.filter((entry) => entry.type !== "session" && typeof entry.id === "string");
68
+ const leaf = treeEntries.at(-1);
69
+ if (!leaf || typeof leaf.id !== "string")
70
+ return [];
71
+ const byId = new Map(treeEntries.map((entry) => [String(entry.id), entry]));
72
+ const branch = [];
73
+ const visited = new Set();
74
+ let current = leaf;
75
+ while (current && typeof current.id === "string") {
76
+ if (visited.has(current.id)) {
77
+ diagnostics.push({ message: `session entry cycle at ${current.id}`, path });
78
+ break;
79
+ }
80
+ visited.add(current.id);
81
+ branch.push(current);
82
+ if (current.parentId === null || current.parentId === undefined)
83
+ break;
84
+ if (typeof current.parentId !== "string" || !byId.has(current.parentId)) {
85
+ diagnostics.push({
86
+ message: `missing parent ${String(current.parentId)} for ${current.id}`,
87
+ path,
88
+ });
89
+ break;
90
+ }
91
+ current = byId.get(current.parentId);
92
+ }
93
+ return branch.reverse();
94
+ }
95
+ function toolCalls(message, maxTextChars, maxToolCalls) {
96
+ return contentBlocks(message)
97
+ .filter((block) => block.type === "toolCall" &&
98
+ typeof block.id === "string" &&
99
+ typeof block.name === "string")
100
+ .slice(0, maxToolCalls)
101
+ .map((block) => ({
102
+ arguments: redactSessionEvidenceValue(block.arguments, maxTextChars),
103
+ id: String(block.id),
104
+ name: String(block.name),
105
+ }));
106
+ }
107
+ export function readSessionEvidence(path, options = {}) {
108
+ const maxTextChars = Math.max(1, options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS);
109
+ const maxToolCalls = Math.max(1, options.maxToolCalls ?? Limits.SESSION_EVIDENCE_MAX_TOOL_CALLS);
110
+ const maxTurns = Math.max(1, options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS);
111
+ const read = readJsonlFileResilient(path);
112
+ const diagnostics = [...read.diagnostics];
113
+ const header = read.records.find((entry) => entry.type === "session");
114
+ const branch = activeBranch(read.records, path, diagnostics);
115
+ const turns = [];
116
+ let pendingUser;
117
+ let currentTurn;
118
+ for (const entry of branch) {
119
+ if (entry.type !== "message")
120
+ continue;
121
+ const message = asRecord(entry.message);
122
+ const role = message.role;
123
+ if (role === "user") {
124
+ pendingUser = {
125
+ id: String(entry.id),
126
+ text: contentText(message, "text", maxTextChars),
127
+ };
128
+ currentTurn = undefined;
129
+ continue;
130
+ }
131
+ if (role === "assistant") {
132
+ currentTurn = {
133
+ ...(typeof entry.id === "string" ? { assistantEntryId: entry.id } : {}),
134
+ ...(contentText(message, "text", maxTextChars)
135
+ ? { assistantText: contentText(message, "text", maxTextChars) }
136
+ : {}),
137
+ ...(typeof message.errorMessage === "string"
138
+ ? { error: boundedText(message.errorMessage, maxTextChars) }
139
+ : {}),
140
+ index: turns.length + 1,
141
+ ...(typeof message.model === "string" ? { model: message.model } : {}),
142
+ ...(typeof message.provider === "string"
143
+ ? { provider: message.provider }
144
+ : {}),
145
+ ...(typeof message.stopReason === "string"
146
+ ? { stopReason: message.stopReason }
147
+ : {}),
148
+ ...(contentText(message, "thinking", maxTextChars)
149
+ ? { thinking: contentText(message, "thinking", maxTextChars) }
150
+ : {}),
151
+ ...(typeof entry.timestamp === "string"
152
+ ? { timestamp: entry.timestamp }
153
+ : {}),
154
+ toolCalls: toolCalls(message, maxTextChars, maxToolCalls),
155
+ unmatchedToolResults: 0,
156
+ ...(message.usage !== undefined
157
+ ? { usage: redactSessionEvidenceValue(message.usage, maxTextChars) }
158
+ : {}),
159
+ ...(pendingUser
160
+ ? {
161
+ userEntryId: pendingUser.id,
162
+ ...(pendingUser.text ? { userText: pendingUser.text } : {}),
163
+ }
164
+ : {}),
165
+ };
166
+ pendingUser = undefined;
167
+ turns.push(currentTurn);
168
+ continue;
169
+ }
170
+ if (role === "toolResult" && currentTurn) {
171
+ const callId = typeof message.toolCallId === "string" ? message.toolCallId : "";
172
+ const call = currentTurn.toolCalls.find((item) => item.id === callId);
173
+ if (!call) {
174
+ currentTurn.unmatchedToolResults += 1;
175
+ continue;
176
+ }
177
+ call.result = redactSessionEvidenceValue(message.content, maxTextChars);
178
+ call.resultError = message.isError === true;
179
+ }
180
+ }
181
+ if (pendingUser) {
182
+ turns.push({
183
+ index: turns.length + 1,
184
+ toolCalls: [],
185
+ unmatchedToolResults: 0,
186
+ userEntryId: pendingUser.id,
187
+ ...(pendingUser.text ? { userText: pendingUser.text } : {}),
188
+ });
189
+ }
190
+ const firstVisibleIndex = Math.max(0, turns.length - maxTurns);
191
+ const visibleTurns = turns.slice(firstVisibleIndex).map((turn, index) => ({
192
+ ...turn,
193
+ index: firstVisibleIndex + index + 1,
194
+ }));
195
+ return {
196
+ ...(branch.at(-1)?.id ? { activeLeafId: String(branch.at(-1)?.id) } : {}),
197
+ diagnostics,
198
+ path,
199
+ ...(header ? { session: asRecord(header) } : {}),
200
+ totalTurns: turns.length,
201
+ truncated: turns.length > visibleTurns.length,
202
+ turns: visibleTurns,
203
+ };
204
+ }
@@ -3,6 +3,9 @@
3
3
  * Zones: compact text summaries, verbose JSON switching, next-action rendering
4
4
  * Owns model-facing response helpers shared by public tool execution paths
5
5
  */
6
+ export declare function withLeadingLineBreak(text: string): string;
7
+ export declare function spaceToolResult<T>(result: T): T;
8
+ export declare function spaceToolError(error: unknown): unknown;
6
9
  export declare function asRecord(value: unknown): Record<string, unknown>;
7
10
  export declare function jsonText(value: unknown): string;
8
11
  export declare function compactPreview(value: unknown, maxLength?: number): string | undefined;
@@ -5,6 +5,35 @@
5
5
  */
6
6
  import * as Limits from "./limits.js";
7
7
  import * as ToolsMailbox from "./tools-mailbox.js";
8
+ export function withLeadingLineBreak(text) {
9
+ return `\n${text.replace(/^\n+/, "")}`;
10
+ }
11
+ export function spaceToolResult(result) {
12
+ if (!result || typeof result !== "object")
13
+ return result;
14
+ const content = result.content;
15
+ if (!Array.isArray(content))
16
+ return result;
17
+ return {
18
+ ...result,
19
+ content: content.map((item) => item &&
20
+ typeof item === "object" &&
21
+ item.type === "text" &&
22
+ typeof item.text === "string"
23
+ ? {
24
+ ...item,
25
+ text: withLeadingLineBreak(item.text),
26
+ }
27
+ : item),
28
+ };
29
+ }
30
+ export function spaceToolError(error) {
31
+ if (error instanceof Error) {
32
+ error.message = withLeadingLineBreak(error.message);
33
+ return error;
34
+ }
35
+ return new Error(withLeadingLineBreak(String(error)));
36
+ }
8
37
  export function asRecord(value) {
9
38
  return value && typeof value === "object" && !Array.isArray(value)
10
39
  ? value