@zachwill/pi-orchestrate 0.9.2 → 0.10.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/extension/catalog/definition.ts +89 -0
- package/extension/{catalog.ts → catalog/discovery.ts} +4 -4
- package/extension/index.ts +17 -36
- package/extension/orchestration/admission.ts +297 -0
- package/extension/{domain.ts → orchestration/model.ts} +27 -89
- package/extension/{runtime.ts → orchestration/service.ts} +178 -467
- package/extension/{worker-settlement.ts → orchestration/settlement.ts} +67 -21
- package/extension/package-root.ts +3 -0
- package/extension/{contract.ts → parent/contract.ts} +2 -2
- package/extension/{delivery.ts → parent/delivery.ts} +5 -8
- package/extension/parent/dispatch-policy.ts +49 -0
- package/extension/{host.ts → parent/process-host.ts} +27 -24
- package/extension/{presentation.ts → pi/presentation.ts} +29 -23
- package/extension/pi/tool-renderer.ts +422 -0
- package/extension/pi/tools.ts +470 -0
- package/extension/worker/child-sessions.ts +291 -0
- package/extension/{worker-session.ts → worker/session.ts} +23 -287
- package/package.json +2 -1
- package/extension/tools.ts +0 -837
- /package/extension/{tui.ts → pi/tui.ts} +0 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
getMarkdownTheme,
|
|
5
|
+
keyHint,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
Container,
|
|
9
|
+
Markdown,
|
|
10
|
+
Spacer,
|
|
11
|
+
Text,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
wrapTextWithAnsi,
|
|
14
|
+
type Component,
|
|
15
|
+
} from "@earendil-works/pi-tui";
|
|
16
|
+
import { Result, Schema } from "effect";
|
|
17
|
+
import {
|
|
18
|
+
disposeComponent,
|
|
19
|
+
formatElapsed,
|
|
20
|
+
resultAppearance,
|
|
21
|
+
WidthBoundComponent,
|
|
22
|
+
} from "./tui.js";
|
|
23
|
+
import {
|
|
24
|
+
decodeInlineWorkerToolDetails,
|
|
25
|
+
type InlineWorkerSettlementDetails,
|
|
26
|
+
WorkerSettlement,
|
|
27
|
+
} from "../orchestration/settlement.js";
|
|
28
|
+
|
|
29
|
+
const MAX_INSTRUCTION_PREVIEW_LINES = 2;
|
|
30
|
+
|
|
31
|
+
const AcceptedRunRenderDetails = Schema.Struct({
|
|
32
|
+
mode: Schema.Literal("async"),
|
|
33
|
+
run_id: WorkerSettlement.fields.runId,
|
|
34
|
+
worker_id: WorkerSettlement.fields.workerId,
|
|
35
|
+
});
|
|
36
|
+
const UnavailableWorkerRenderDetails = Schema.Union([
|
|
37
|
+
Schema.Struct({ result: Schema.Unknown }),
|
|
38
|
+
Schema.Struct({ worker_id: Schema.Unknown }),
|
|
39
|
+
]);
|
|
40
|
+
const decodeAcceptedRunRenderDetails = Schema.decodeUnknownResult(
|
|
41
|
+
AcceptedRunRenderDetails,
|
|
42
|
+
);
|
|
43
|
+
const decodeUnavailableWorkerRenderDetails = Schema.decodeUnknownResult(
|
|
44
|
+
UnavailableWorkerRenderDetails,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
interface RenderOptions {
|
|
48
|
+
readonly isPartial: boolean;
|
|
49
|
+
readonly expanded: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface RenderContext {
|
|
53
|
+
readonly isError: boolean;
|
|
54
|
+
readonly lastComponent?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const orchestrateToolRenderer = {
|
|
58
|
+
renderCall(args: unknown, theme: Theme, { expanded }: RenderOptions) {
|
|
59
|
+
return renderDispatchCall(theme, args, expanded);
|
|
60
|
+
},
|
|
61
|
+
renderResult(
|
|
62
|
+
result: AgentToolResult<unknown>,
|
|
63
|
+
{ isPartial, expanded }: RenderOptions,
|
|
64
|
+
theme: Theme,
|
|
65
|
+
context: RenderContext,
|
|
66
|
+
) {
|
|
67
|
+
return renderOrchestrationResult(
|
|
68
|
+
result,
|
|
69
|
+
isPartial,
|
|
70
|
+
expanded,
|
|
71
|
+
theme,
|
|
72
|
+
context.isError,
|
|
73
|
+
context.lastComponent,
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const workerStatusToolRenderer = {
|
|
79
|
+
renderCall(_args: unknown, theme: Theme) {
|
|
80
|
+
return new Text(theme.fg("toolTitle", theme.bold("worker_status")), 0, 0);
|
|
81
|
+
},
|
|
82
|
+
renderResult(
|
|
83
|
+
result: AgentToolResult<unknown>,
|
|
84
|
+
{ isPartial }: RenderOptions,
|
|
85
|
+
theme: Theme,
|
|
86
|
+
context: RenderContext,
|
|
87
|
+
) {
|
|
88
|
+
return renderDiagnosticsResult(result, isPartial, context.isError, theme);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const interactiveSendToolRenderer = {
|
|
93
|
+
renderCall(args: unknown, theme: Theme, { expanded }: RenderOptions) {
|
|
94
|
+
const fields = objectFields(args);
|
|
95
|
+
return renderInteractiveMessageCall(
|
|
96
|
+
theme,
|
|
97
|
+
"interactive_send",
|
|
98
|
+
fields.worker_id,
|
|
99
|
+
fields.instructions,
|
|
100
|
+
expanded,
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
renderResult: orchestrateToolRenderer.renderResult,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const workerAbortToolRenderer = {
|
|
107
|
+
renderCall(args: unknown, theme: Theme) {
|
|
108
|
+
const fields = objectFields(args);
|
|
109
|
+
const workerIds = Array.isArray(fields.worker_ids)
|
|
110
|
+
? fields.worker_ids
|
|
111
|
+
: undefined;
|
|
112
|
+
const target = workerIds
|
|
113
|
+
? `${workerIds.length} worker${workerIds.length === 1 ? "" : "s"}`
|
|
114
|
+
: fields.all === true ? "all workers" : "";
|
|
115
|
+
return renderCompactCall(theme, "worker_abort", target);
|
|
116
|
+
},
|
|
117
|
+
renderResult(
|
|
118
|
+
result: AgentToolResult<unknown>,
|
|
119
|
+
{ isPartial }: RenderOptions,
|
|
120
|
+
theme: Theme,
|
|
121
|
+
context: RenderContext,
|
|
122
|
+
) {
|
|
123
|
+
return renderSimpleResult(
|
|
124
|
+
result,
|
|
125
|
+
context.isError,
|
|
126
|
+
isPartial ? "Requesting worker stop…" : "Worker stop requested",
|
|
127
|
+
theme,
|
|
128
|
+
"warning",
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const interactiveCloseToolRenderer = {
|
|
134
|
+
renderCall(args: unknown, theme: Theme) {
|
|
135
|
+
return renderCompactCall(
|
|
136
|
+
theme,
|
|
137
|
+
"interactive_close",
|
|
138
|
+
objectFields(args).worker_id,
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
renderResult(
|
|
142
|
+
result: AgentToolResult<unknown>,
|
|
143
|
+
{ isPartial }: RenderOptions,
|
|
144
|
+
theme: Theme,
|
|
145
|
+
context: RenderContext,
|
|
146
|
+
) {
|
|
147
|
+
return renderSimpleResult(
|
|
148
|
+
result,
|
|
149
|
+
context.isError,
|
|
150
|
+
isPartial ? "Closing worker…" : "✓ Worker closed",
|
|
151
|
+
theme,
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function renderDispatchCall(
|
|
157
|
+
theme: Theme,
|
|
158
|
+
task: unknown,
|
|
159
|
+
expanded: boolean,
|
|
160
|
+
): Component {
|
|
161
|
+
const fields = isRecord(task) ? task : {};
|
|
162
|
+
const container = new Container();
|
|
163
|
+
container.addChild(new Text(
|
|
164
|
+
theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(fields.worker)),
|
|
165
|
+
0, 0,
|
|
166
|
+
));
|
|
167
|
+
container.addChild(new Text(
|
|
168
|
+
`${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(fields.title)))}`,
|
|
169
|
+
0, 0,
|
|
170
|
+
));
|
|
171
|
+
if (expanded) {
|
|
172
|
+
container.addChild(new Text(safeTerminalText(fields.instructions), 2, 0));
|
|
173
|
+
return new WidthBoundComponent(container);
|
|
174
|
+
}
|
|
175
|
+
container.addChild(new InstructionPreview(fields.instructions, theme));
|
|
176
|
+
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
|
|
177
|
+
return new WidthBoundComponent(container);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
class InstructionPreview implements Component {
|
|
181
|
+
constructor(
|
|
182
|
+
private readonly instructions: unknown,
|
|
183
|
+
private readonly theme: Theme,
|
|
184
|
+
) {}
|
|
185
|
+
render(width: number): string[] {
|
|
186
|
+
const bounded = Math.max(1, width);
|
|
187
|
+
const contentWidth = Math.max(1, bounded - 2);
|
|
188
|
+
const characterLimit = Math.max(256, Math.min(4096, contentWidth * 3));
|
|
189
|
+
const preview = compactInstructionPreview(this.instructions, characterLimit);
|
|
190
|
+
if (!preview.text) return [];
|
|
191
|
+
|
|
192
|
+
const wrapped = wrapTextWithAnsi(preview.text, contentWidth);
|
|
193
|
+
const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
|
|
194
|
+
if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
|
|
195
|
+
const lastIndex = previewLines.length - 1;
|
|
196
|
+
previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
|
|
197
|
+
}
|
|
198
|
+
return previewLines.map((line) =>
|
|
199
|
+
truncateToWidth(this.theme.fg("dim", ` ${line}`), bounded, "…")
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
invalidate(): void {}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderInteractiveMessageCall(
|
|
206
|
+
theme: Theme,
|
|
207
|
+
tool: string,
|
|
208
|
+
workerId: unknown,
|
|
209
|
+
instructions: unknown,
|
|
210
|
+
expanded: boolean,
|
|
211
|
+
): Component {
|
|
212
|
+
const container = new Container();
|
|
213
|
+
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(workerId)), 0, 0));
|
|
214
|
+
if (expanded) container.addChild(new Text(safeTerminalText(instructions), 2, 0));
|
|
215
|
+
else {
|
|
216
|
+
container.addChild(new Text(`${theme.fg("accent", "→")} ${truncateInstruction(instructions, 240)}`, 0, 0));
|
|
217
|
+
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full message")), 0, 0));
|
|
218
|
+
}
|
|
219
|
+
return new WidthBoundComponent(container);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function safeTerminalText(value: unknown): string {
|
|
223
|
+
const text = typeof value === "string" ? value : value == null ? "" : String(value);
|
|
224
|
+
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
|
|
225
|
+
const code = character.charCodeAt(0);
|
|
226
|
+
return code === 0x7f ? "␡" : String.fromCodePoint(0x2400 + code);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function compactInstructionPreview(instructions: unknown, characterLimit: number): { text: string; truncated: boolean } {
|
|
231
|
+
const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
|
|
232
|
+
const source = text.slice(0, characterLimit);
|
|
233
|
+
return {
|
|
234
|
+
text: safeTerminalText(source).replace(/\s+/g, " ").trim(),
|
|
235
|
+
truncated: source.length < text.length,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function firstInstructionLine(instructions: unknown): string | undefined {
|
|
240
|
+
const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
|
|
241
|
+
return text.split(/\r\n?|\n/).find((line) => line.trim().length > 0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function truncateInstruction(instructions: unknown, limit: number): string {
|
|
245
|
+
const first = firstInstructionLine(instructions) ?? "";
|
|
246
|
+
return first.length > limit ? `${first.slice(0, limit - 1)}…` : first;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderCompactCall(theme: Theme, tool: string, target: unknown): Text {
|
|
250
|
+
return new Text(
|
|
251
|
+
theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(target)),
|
|
252
|
+
0,
|
|
253
|
+
0,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderOrchestrationResult(
|
|
258
|
+
result: AgentToolResult<unknown>,
|
|
259
|
+
isPartial: boolean,
|
|
260
|
+
expanded: boolean,
|
|
261
|
+
theme: Theme,
|
|
262
|
+
isError: boolean,
|
|
263
|
+
lastComponent: unknown,
|
|
264
|
+
): Component {
|
|
265
|
+
if (isError) {
|
|
266
|
+
return new WidthBoundComponent(renderSimpleResult(
|
|
267
|
+
result,
|
|
268
|
+
true,
|
|
269
|
+
firstResultLine(result) || "Worker operation failed",
|
|
270
|
+
theme,
|
|
271
|
+
"warning",
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
const details = result.details;
|
|
275
|
+
if (Result.isSuccess(decodeAcceptedRunRenderDetails(details))) {
|
|
276
|
+
return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
|
|
277
|
+
}
|
|
278
|
+
const inlineResult = readInlineResult(details);
|
|
279
|
+
if (inlineResult) {
|
|
280
|
+
const component = lastComponent instanceof InlineResultComponent
|
|
281
|
+
? lastComponent
|
|
282
|
+
: new InlineResultComponent(theme);
|
|
283
|
+
component.update(inlineResult, isPartial, expanded);
|
|
284
|
+
return component;
|
|
285
|
+
}
|
|
286
|
+
if (Result.isSuccess(decodeUnavailableWorkerRenderDetails(details))) {
|
|
287
|
+
return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
|
|
288
|
+
}
|
|
289
|
+
if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
|
|
290
|
+
return new WidthBoundComponent(renderSimpleResult(
|
|
291
|
+
result,
|
|
292
|
+
false,
|
|
293
|
+
firstResultLine(result) || "Work sent",
|
|
294
|
+
theme,
|
|
295
|
+
"warning",
|
|
296
|
+
));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface RenderedInlineSettlement {
|
|
300
|
+
worker: string;
|
|
301
|
+
title: string;
|
|
302
|
+
status: InlineWorkerSettlementDetails["status"];
|
|
303
|
+
response: string;
|
|
304
|
+
elapsed?: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
class InlineResultComponent implements Component {
|
|
308
|
+
private result: RenderedInlineSettlement | undefined;
|
|
309
|
+
private partial = false;
|
|
310
|
+
private expanded = false;
|
|
311
|
+
private child: Component = new Container();
|
|
312
|
+
constructor(private readonly theme: Theme) {}
|
|
313
|
+
update(result: RenderedInlineSettlement, partial: boolean, expanded: boolean): void {
|
|
314
|
+
this.result = result;
|
|
315
|
+
this.partial = partial;
|
|
316
|
+
this.expanded = expanded;
|
|
317
|
+
this.rebuild();
|
|
318
|
+
}
|
|
319
|
+
render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
|
|
320
|
+
invalidate(): void { this.rebuild(); }
|
|
321
|
+
dispose(): void { disposeComponent(this.child); }
|
|
322
|
+
private rebuild(): void {
|
|
323
|
+
disposeComponent(this.child);
|
|
324
|
+
const container = new Container();
|
|
325
|
+
const result = this.result;
|
|
326
|
+
if (!result) {
|
|
327
|
+
this.child = container;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const appearance = resultAppearance(result.status, "ready for follow-up");
|
|
331
|
+
const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
|
|
332
|
+
const title = this.theme.bold(result.title);
|
|
333
|
+
const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
|
|
334
|
+
const header = [
|
|
335
|
+
this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
|
|
336
|
+
workerName,
|
|
337
|
+
...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
|
|
338
|
+
].join(" · ");
|
|
339
|
+
container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
|
|
340
|
+
if (result.response) {
|
|
341
|
+
const markdown = new Markdown(result.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
|
|
342
|
+
container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
|
|
343
|
+
}
|
|
344
|
+
container.addChild(new Spacer(1));
|
|
345
|
+
if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Receiving worker response…"), 0, 0));
|
|
346
|
+
else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full response")), 0, 0));
|
|
347
|
+
this.child = container;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readInlineResult(details: unknown): RenderedInlineSettlement | undefined {
|
|
352
|
+
const decoded = decodeInlineWorkerToolDetails(details);
|
|
353
|
+
if (Result.isFailure(decoded)) return undefined;
|
|
354
|
+
const settlement = decoded.success.result;
|
|
355
|
+
const outcome = settlement.outcome;
|
|
356
|
+
const message = outcome.status === "failed" || outcome.status === "aborted"
|
|
357
|
+
? outcome.message
|
|
358
|
+
: undefined;
|
|
359
|
+
const assistantText = outcome.assistantText;
|
|
360
|
+
const response = [message, assistantText]
|
|
361
|
+
.filter((item): item is string => typeof item === "string" && item.length > 0)
|
|
362
|
+
.join("\n\n");
|
|
363
|
+
return {
|
|
364
|
+
worker: settlement.worker,
|
|
365
|
+
title: settlement.title,
|
|
366
|
+
status: settlement.status,
|
|
367
|
+
response,
|
|
368
|
+
elapsed: formatElapsed(settlement.settledAt - settlement.startedAt),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderDiagnosticsResult(
|
|
373
|
+
result: AgentToolResult<unknown>,
|
|
374
|
+
isPartial: boolean,
|
|
375
|
+
isError: boolean,
|
|
376
|
+
theme: Theme,
|
|
377
|
+
): Text {
|
|
378
|
+
if (isError) {
|
|
379
|
+
return renderSimpleResult(
|
|
380
|
+
result,
|
|
381
|
+
true,
|
|
382
|
+
firstResultLine(result) || "Worker diagnostics failed",
|
|
383
|
+
theme,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
|
|
387
|
+
const details = result.details;
|
|
388
|
+
if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
|
|
389
|
+
const workers = details.state.workers.filter(isRecord);
|
|
390
|
+
const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
|
|
391
|
+
const ready = workers.filter((worker) => worker.status === "ready").length;
|
|
392
|
+
const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;
|
|
393
|
+
const facts = [active ? `${active} active` : "No active workers", ready ? `${ready} available for follow-up` : undefined, diagnostics ? `${diagnostics} catalog diagnostic${diagnostics === 1 ? "" : "s"}` : undefined].filter(Boolean);
|
|
394
|
+
return new Text(theme.fg("muted", facts.join(" · ")), 0, 0);
|
|
395
|
+
}
|
|
396
|
+
return new Text(theme.fg("muted", firstResultLine(result) || "Diagnostics unavailable"), 0, 0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderSimpleResult(
|
|
400
|
+
result: AgentToolResult<unknown>,
|
|
401
|
+
isError: boolean,
|
|
402
|
+
message: string,
|
|
403
|
+
theme: Theme,
|
|
404
|
+
normalColor: "success" | "warning" = "success",
|
|
405
|
+
): Text {
|
|
406
|
+
const text = isError ? firstResultLine(result) || message : message;
|
|
407
|
+
return new Text(theme.fg(isError ? "error" : normalColor, text), 0, 0);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
|
|
411
|
+
const first = result.content[0];
|
|
412
|
+
if (first?.type !== "text") return undefined;
|
|
413
|
+
return first.text.split("\n").find((line) => line.trim())?.trim();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function objectFields(value: unknown): Record<string, unknown> {
|
|
417
|
+
return isRecord(value) ? value : {};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
421
|
+
return typeof value === "object" && value !== null;
|
|
422
|
+
}
|