pi-agent-flow 1.2.0 → 1.2.3

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/src/index.ts CHANGED
@@ -23,16 +23,22 @@ import { mapFlowConcurrent, runFlow } from "./flow.js";
23
23
  import {
24
24
  type SingleResult,
25
25
  type FlowDetails,
26
+ type CompressedFlowResult,
27
+ type FileEntry,
28
+ type CommandEntry,
26
29
  emptyFlowUsage,
27
30
  isFlowError,
28
31
  isFlowSuccess,
32
+ getFlowOutput,
29
33
  } from "./types.js";
34
+ import { extractStructuredOutput } from "./structured-output.js";
30
35
  import { createBatchTool, createBatchReadTool } from "./batch.js";
31
36
  import {
32
37
  createWebTool,
33
38
  looksLikeUrlPrompt,
34
39
  looksLikeWebSearchPrompt,
35
40
  } from "./web-tool.js";
41
+ import { createTimedBashToolDefinition } from "./timed-bash.js";
36
42
 
37
43
  // ---------------------------------------------------------------------------
38
44
  // Limits
@@ -471,6 +477,152 @@ function stripReasoningFromAssistantMessage(message: any): {
471
477
  function isJsonEqual(a: any, b: any): boolean {
472
478
  return JSON.stringify(a) === JSON.stringify(b);
473
479
  }
480
+ // ---------------------------------------------------------------------------
481
+ // Flow result compression
482
+ // ---------------------------------------------------------------------------
483
+
484
+ /**
485
+ * Module-level cache populated after each flow tool execution.
486
+ * Maps toolCallId → compressed flow result.
487
+ * Consumed by compressFlowToolResults at fork time.
488
+ */
489
+ const flowResultCache = new Map<string, CompressedFlowResult[]>();
490
+
491
+ /**
492
+ * Build compressed representations of flow results and cache them by toolCallId.
493
+ */
494
+ function cacheFlowResults(toolCallId: string, results: SingleResult[]): void {
495
+ for (const result of results) {
496
+ const so = result.structuredOutput;
497
+ if (!so) continue;
498
+ const compressed: CompressedFlowResult = {
499
+ type: result.type,
500
+ status: isFlowError(result) ? "failed" : "accomplished",
501
+ };
502
+ if (so.files.length > 0) compressed.files = so.files;
503
+ if (so.commands.length > 0) compressed.commands = so.commands;
504
+ if (result.errorMessage) compressed.error = result.errorMessage;
505
+ const existing = flowResultCache.get(toolCallId) ?? [];
506
+ existing.push(compressed);
507
+ flowResultCache.set(toolCallId, existing);
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Render a compressed flow result as compact text for child context.
513
+ */
514
+ function renderCompressedFlowResult(r: CompressedFlowResult): string {
515
+ const parts: string[] = [`[Flow: ${r.type} ${r.status}]`];
516
+ if (r.files?.length) {
517
+ const fileLines = r.files.map((f) => {
518
+ const role = f.role ? ` (${f.role})` : "";
519
+ const desc = f.description ? ` — ${f.description}` : "";
520
+ return ` ${f.path}${role}${desc}`;
521
+ });
522
+ parts.push(`Files:\n${fileLines.join("\n")}`);
523
+ }
524
+ if (r.commands?.length) {
525
+ const cmdLines = r.commands.map((c) => {
526
+ const result = c.result ? ` (${c.result})` : "";
527
+ const purpose = c.purpose ? ` — ${c.purpose}` : "";
528
+ return ` ${c.tool ?? "cmd"}: ${c.command}${result}${purpose}`;
529
+ });
530
+ parts.push(`Commands:\n${cmdLines.join("\n")}`);
531
+ }
532
+ if (r.error) parts.push(`Error: ${r.error}`);
533
+ return parts.join("\n");
534
+ }
535
+
536
+ /**
537
+ * Compress flow tool results in a sanitized session snapshot.
538
+ *
539
+ * Scans for tool result messages that correspond to flow invocations
540
+ * and replaces their content with compact compressed output.
541
+ */
542
+ export function compressFlowToolResults(snapshot: string, cache: Map<string, CompressedFlowResult[]>): string {
543
+ if (cache.size === 0) return snapshot;
544
+
545
+ const lines = snapshot.trimEnd().split("\n");
546
+ const result: string[] = [];
547
+
548
+ // First pass: map toolCallId → tool name from assistant messages
549
+ const toolCallIdToName = new Map<string, string>();
550
+ for (const line of lines) {
551
+ let entry: any;
552
+ try { entry = JSON.parse(line); } catch { continue; }
553
+ if (entry?.type !== "message" || entry.message?.role !== "assistant") continue;
554
+ const content = entry.message.content;
555
+ if (!Array.isArray(content)) continue;
556
+ for (const part of content) {
557
+ if (part.type === "toolCall" && part.toolCallId && part.name) {
558
+ toolCallIdToName.set(part.toolCallId, part.name);
559
+ }
560
+ }
561
+ }
562
+
563
+ // Second pass: compress flow tool results
564
+ for (const line of lines) {
565
+ let entry: any;
566
+ try { entry = JSON.parse(line); } catch { result.push(line); continue; }
567
+
568
+ if (entry?.type !== "message" || entry.message?.role !== "tool") {
569
+ result.push(line);
570
+ continue;
571
+ }
572
+
573
+ // Extract toolCallId — either from message-level or content-level toolResult
574
+ let toolCallId: string | undefined;
575
+ if (typeof entry.message.toolCallId === "string") {
576
+ toolCallId = entry.message.toolCallId;
577
+ } else if (Array.isArray(entry.message.content)) {
578
+ for (const part of entry.message.content) {
579
+ if (part.type === "toolResult" && part.toolCallId) {
580
+ toolCallId = part.toolCallId;
581
+ break;
582
+ }
583
+ }
584
+ }
585
+
586
+ if (!toolCallId) { result.push(line); continue; }
587
+
588
+ const toolName = toolCallIdToName.get(toolCallId);
589
+ if (toolName !== "flow") { result.push(line); continue; }
590
+
591
+ const compressed = cache.get(toolCallId);
592
+ if (!compressed || compressed.length === 0) { result.push(line); continue; }
593
+
594
+ const rendered = compressed.map(renderCompressedFlowResult).join("\n\n");
595
+
596
+ // Replace content in the tool result message
597
+ if (typeof entry.message.toolCallId === "string") {
598
+ // Format 1: toolCallId at message level, content is text array
599
+ entry = {
600
+ ...entry,
601
+ message: {
602
+ ...entry.message,
603
+ content: [{ type: "text", text: rendered }],
604
+ },
605
+ };
606
+ } else {
607
+ // Format 2: toolCallId inside content array
608
+ entry = {
609
+ ...entry,
610
+ message: {
611
+ ...entry.message,
612
+ content: entry.message.content.map((part: any) =>
613
+ part.type === "toolResult" && part.toolCallId === toolCallId
614
+ ? { ...part, content: rendered }
615
+ : part,
616
+ ),
617
+ },
618
+ };
619
+ }
620
+
621
+ result.push(JSON.stringify(entry));
622
+ }
623
+
624
+ return `${result.join("\n")}\n`;
625
+ }
474
626
 
475
627
  /**
476
628
  * Sanitize a fork session snapshot JSONL to remove only non-inheritable
@@ -533,7 +685,8 @@ function sanitizeForkSnapshot(snapshot: string | null): string | null {
533
685
  sanitizedLines.push(changed ? JSON.stringify(entry) : line);
534
686
  }
535
687
 
536
- return `${sanitizedLines.join("\n")}\n`;
688
+ const sanitized = `${sanitizedLines.join("\n")}\n`;
689
+ return compressFlowToolResults(sanitized, flowResultCache);
537
690
  }
538
691
 
539
692
  function computeActiveTools(optimize: boolean): string[] {
@@ -582,6 +735,8 @@ export default function (pi: ExtensionAPI) {
582
735
 
583
736
  // toolOptimize: CLI flag > env var > settings.json > default (true)
584
737
  let toolOptimize = true;
738
+ // structuredOutput: settings.json > default (true)
739
+ let structuredOutput = true;
585
740
  const envToolOptimize = process.env[FLOW_TOOL_OPTIMIZE_ENV];
586
741
  if (envToolOptimize !== undefined) {
587
742
  const parsed = parseBoolean(envToolOptimize);
@@ -613,6 +768,9 @@ export default function (pi: ExtensionAPI) {
613
768
  if (typeof flowSettings.toolOptimize === "boolean") {
614
769
  toolOptimize = flowSettings.toolOptimize;
615
770
  }
771
+ if (typeof flowSettings.structuredOutput === "boolean") {
772
+ structuredOutput = flowSettings.structuredOutput;
773
+ }
616
774
  }
617
775
 
618
776
  // Only restrict tools for the main orchestrator (depth 0).
@@ -627,6 +785,12 @@ export default function (pi: ExtensionAPI) {
627
785
  pi.registerTool(createBatchReadTool());
628
786
  pi.registerTool(createBatchTool());
629
787
  }
788
+
789
+ // Override built-in bash with timed wrapper so the LLM sees execution-time classification.
790
+ const timedBash = createTimedBashToolDefinition(ctx.cwd);
791
+ if (timedBash) {
792
+ pi.registerTool(timedBash);
793
+ }
630
794
  });
631
795
 
632
796
  // Re-apply active tools every turn to survive registry refreshes.
@@ -776,8 +940,9 @@ flow [type] accomplished
776
940
  ].join("\n"),
777
941
  parameters: FlowParams,
778
942
 
779
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
943
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
780
944
  const discovery = discoverFlows(ctx.cwd, "all");
945
+ flowResultCache.clear();
781
946
  const { flows } = discovery;
782
947
  const makeDetails = makeFlowDetailsFactory(discovery.projectFlowsDir);
783
948
 
@@ -940,6 +1105,7 @@ flow [type] accomplished
940
1105
  maxDepth: effectiveMaxDepth,
941
1106
  preventCycles,
942
1107
  toolOptimize,
1108
+ structuredOutput,
943
1109
  model: candidateModel,
944
1110
  signal,
945
1111
  onUpdate: (partial) => {
@@ -970,6 +1136,7 @@ flow [type] accomplished
970
1136
  return result;
971
1137
  });
972
1138
 
1139
+ cacheFlowResults(toolCallId, results);
973
1140
  // Build tool result with FULL flow output — no truncation
974
1141
  const successCount = results.filter((r) => isFlowSuccess(r)).length;
975
1142
  const flowReports = results.map((r) => {
package/src/render.ts CHANGED
@@ -241,6 +241,42 @@ function renderFlowExpanded(
241
241
  // Flow report (structured output)
242
242
  container.addChild(new Spacer(1));
243
243
  container.addChild(new Text(theme.fg("muted", sectionHeader("report")), 0, 0));
244
+
245
+ // Structured output summary (compact badge when available)
246
+ if (r.structuredOutput) {
247
+ const so = r.structuredOutput;
248
+ const statusColor = so.status === "complete" ? "success" : so.status === "partial" ? "warning" : "error";
249
+ container.addChild(new Text(
250
+ `${theme.fg(statusColor, `[${so.status}]`)} ${theme.fg("dim", so.summary)}`,
251
+ 0, 0,
252
+ ));
253
+ if (so.files.length > 0) {
254
+ container.addChild(new Text(theme.fg("dim", `Files: ${so.files.map((f) => f.path).join(", ")}`), 0, 0));
255
+ }
256
+ if (so.commands?.length > 0) {
257
+ const cmdLabels = so.commands.map((c) => {
258
+ const short = c.command.length > 30 ? c.command.slice(0, 30) + "..." : c.command;
259
+ return `${c.tool ?? "cmd"}: ${short}`;
260
+ });
261
+ container.addChild(new Text(theme.fg("dim", `Commands: ${cmdLabels.join(", ")}`), 0, 0));
262
+ }
263
+ if (so.notDone.length > 0) {
264
+ const notDoneText = so.notDone.map((item) => {
265
+ const details = [
266
+ item.reason ? `reason: ${item.reason}` : undefined,
267
+ item.blocker ? `blocker: ${item.blocker}` : undefined,
268
+ item.nextStep ? `next: ${item.nextStep}` : undefined,
269
+ ].filter(Boolean).join("; ");
270
+ return details ? `${item.item} (${details})` : item.item;
271
+ }).join("; ");
272
+ container.addChild(new Text(theme.fg("dim", `Not Done: ${notDoneText}`), 0, 0));
273
+ }
274
+ if (so.nextSteps.length > 0) {
275
+ container.addChild(new Text(theme.fg("dim", `Next: ${so.nextSteps.join("; ")}`), 0, 0));
276
+ }
277
+ container.addChild(new Spacer(1));
278
+ }
279
+
244
280
  if (flowOutput) {
245
281
  container.addChild(new Markdown(flowOutput.trim(), 0, 0, mdTheme));
246
282
  } else {
@@ -294,6 +330,9 @@ function renderFlowCollapsed(
294
330
  if (r.exitCode === -1 && streamingText) {
295
331
  const logContent = tailText(streamingText, contentBudget(10));
296
332
  container.addChild(new TruncatedText(`${theme.fg("dim", "└─ msg:")} ${theme.fg("dim", logContent)}`, 0, 0));
333
+ } else if (r.structuredOutput?.summary) {
334
+ const logContent = truncateChars(r.structuredOutput.summary, contentBudget(10));
335
+ container.addChild(new TruncatedText(`${theme.fg("dim", "└─ msg:")} ${theme.fg("dim", logContent)}`, 0, 0));
297
336
  } else if (flowOutput) {
298
337
  const logContent = tailText(flowOutput, contentBudget(10));
299
338
  container.addChild(new TruncatedText(`${theme.fg("dim", "└─ msg:")} ${theme.fg("dim", logContent)}`, 0, 0));
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Structured JSON output extraction from flow responses.
3
+ *
4
+ * Parses a JSON code block from the end of the flow's final assistant text
5
+ * and validates it against the FlowStructuredOutput schema.
6
+ */
7
+
8
+ import type { Action, CommandEntry, FileEntry, FlowStructuredOutput, NotDoneItem } from "./types.js";
9
+
10
+ type FlowStatus = FlowStructuredOutput["status"];
11
+
12
+ type StructuredOutputRecord = {
13
+ version: string;
14
+ status: FlowStatus;
15
+ summary: string;
16
+ files?: FileEntry[];
17
+ actions?: Action[];
18
+ commands?: CommandEntry[];
19
+ notDone?: NotDoneItem[];
20
+ nextSteps?: string[];
21
+ reasoning?: string[];
22
+ notes?: string[];
23
+ extensions?: Record<string, unknown>;
24
+ };
25
+
26
+ const VALID_STATUSES: FlowStatus[] = ["complete", "partial", "blocked", "failed"];
27
+
28
+ function isRecord(value: unknown): value is Record<string, unknown> {
29
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30
+ }
31
+
32
+ function isOptionalArray(record: Record<string, unknown>, key: string): boolean {
33
+ return record[key] === undefined || Array.isArray(record[key]);
34
+ }
35
+
36
+ /** Minimum required fields to consider parsed JSON a valid structured output. */
37
+ function isValidStructuredOutput(obj: unknown): obj is StructuredOutputRecord {
38
+ if (!isRecord(obj)) return false;
39
+ return (
40
+ typeof obj.version === "string" &&
41
+ typeof obj.status === "string" &&
42
+ VALID_STATUSES.includes(obj.status as FlowStatus) &&
43
+ typeof obj.summary === "string" &&
44
+ isOptionalArray(obj, "files") &&
45
+ isOptionalArray(obj, "actions") &&
46
+ isOptionalArray(obj, "commands") &&
47
+ isOptionalArray(obj, "notDone") &&
48
+ isOptionalArray(obj, "nextSteps") &&
49
+ isOptionalArray(obj, "reasoning") &&
50
+ isOptionalArray(obj, "notes")
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Extract a structured JSON output block from the end of an assistant's text.
56
+ *
57
+ * Looks for a final ```json ... ``` code block, parses it, and validates
58
+ * against the FlowStructuredOutput schema. Returns undefined when the block
59
+ * is missing, malformed, or fails validation.
60
+ */
61
+ export function extractStructuredOutput(text: string): FlowStructuredOutput | undefined {
62
+ if (!text) return undefined;
63
+
64
+ // Find the last ```json ... ``` block in the text.
65
+ // Scan backward from the end to handle multiple blocks.
66
+ const allMatches = [...text.matchAll(/```json\s*([\s\S]*?)\s*```/g)];
67
+ const match = allMatches.length > 0 ? allMatches[allMatches.length - 1] : null;
68
+ if (!match || !match[1]) return undefined;
69
+
70
+ const jsonStr = match[1].trim();
71
+ if (!jsonStr) return undefined;
72
+
73
+ let parsed: unknown;
74
+ try {
75
+ parsed = JSON.parse(jsonStr);
76
+ } catch {
77
+ return undefined;
78
+ }
79
+
80
+ if (!isValidStructuredOutput(parsed)) return undefined;
81
+
82
+ // Sanitize: trim string fields and normalize omitted arrays to [] for
83
+ // backward compatibility with earlier structured-output prompts.
84
+ return {
85
+ version: parsed.version.trim(),
86
+ status: parsed.status,
87
+ summary: parsed.summary.trim(),
88
+ files: parsed.files ?? [],
89
+ actions: parsed.actions ?? [],
90
+ commands: parsed.commands ?? [],
91
+ notDone: parsed.notDone ?? [],
92
+ nextSteps: parsed.nextSteps ?? [],
93
+ reasoning: parsed.reasoning ?? [],
94
+ notes: parsed.notes ?? [],
95
+ ...(parsed.extensions !== undefined ? { extensions: parsed.extensions } : {}),
96
+ };
97
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Timed Bash Tool Wrapper
3
+ *
4
+ * Wraps the built-in bash tool to append execution-time classification
5
+ * to every result. This gives the LLM concrete feedback to self-correct
6
+ * strategy (e.g. switch from bash grep to grep tool, batch commands, etc.).
7
+ */
8
+
9
+ import { createBashToolDefinition } from "@mariozechner/pi-coding-agent";
10
+
11
+ export type TimingTier =
12
+ | "normal"
13
+ | "avg"
14
+ | "long"
15
+ | "extreme_long"
16
+ | "very_long";
17
+
18
+ export interface TimingReport {
19
+ tier: TimingTier;
20
+ seconds: number;
21
+ label: string;
22
+ }
23
+
24
+ /** Classify duration into user-defined tiers with actionable feedback. */
25
+ export function classifyDuration(ms: number): TimingReport {
26
+ const s = ms / 1000;
27
+ if (s < 10) {
28
+ return { tier: "normal", seconds: s, label: `${s.toFixed(1)}s (normal)` };
29
+ }
30
+ if (s < 30) {
31
+ return { tier: "avg", seconds: s, label: `${s.toFixed(1)}s (avg) — user feedback: consider improving the current commands or find a better solution` };
32
+ }
33
+ if (s < 60) {
34
+ return {
35
+ tier: "long",
36
+ seconds: s,
37
+ label: `${s.toFixed(1)}s (long) — user feedback: consider improving the whole scripts`,
38
+ };
39
+ }
40
+ if (s < 300) {
41
+ return {
42
+ tier: "extreme_long",
43
+ seconds: s,
44
+ label: `${s.toFixed(1)}s (extreme long) — user feedback: should consider to improve the whole scripts`,
45
+ };
46
+ }
47
+ return {
48
+ tier: "very_long",
49
+ seconds: s,
50
+ label: `${(s / 60).toFixed(1)}min (very long) — user feedback: consider to improve, only run when everything tested with other means`,
51
+ };
52
+ }
53
+
54
+ /** Format the timing appendix that gets appended to bash output. */
55
+ export function formatTimingAppendix(report: TimingReport): string {
56
+ return `\n\n[Execution time: ${report.label}]`;
57
+ }
58
+
59
+ /**
60
+ * Create a timed bash tool definition that wraps the built-in one.
61
+ * Extensions override built-in tools by name, so this replaces the
62
+ * default `bash` tool transparently.
63
+ *
64
+ * Returns `null` if the underlying `createBashToolDefinition` is not
65
+ * available (e.g. test environment or incompatible CLI version).
66
+ */
67
+ export function createTimedBashToolDefinition(
68
+ cwd: string,
69
+ options?: {
70
+ shellPath?: string;
71
+ commandPrefix?: string;
72
+ operations?: any;
73
+ spawnHook?: any;
74
+ },
75
+ ): any {
76
+ let original: any;
77
+ try {
78
+ original = createBashToolDefinition(cwd, options);
79
+ } catch {
80
+ return null;
81
+ }
82
+ if (!original || typeof original.execute !== "function") {
83
+ return null;
84
+ }
85
+
86
+ return {
87
+ ...original,
88
+ async execute(
89
+ toolCallId: string,
90
+ params: { command: string; timeout?: number },
91
+ signal: AbortSignal,
92
+ onUpdate: any,
93
+ ctx: any,
94
+ ) {
95
+ const start = Date.now();
96
+ try {
97
+ const result = await original.execute(
98
+ toolCallId,
99
+ params,
100
+ signal,
101
+ onUpdate,
102
+ ctx,
103
+ );
104
+ const duration = Date.now() - start;
105
+ const report = classifyDuration(duration);
106
+ const appendix = formatTimingAppendix(report);
107
+
108
+ const textItem = result.content?.find(
109
+ (c: any) => c.type === "text",
110
+ );
111
+ if (textItem && typeof textItem.text === "string") {
112
+ textItem.text += appendix;
113
+ } else if (result.content) {
114
+ result.content.push({ type: "text", text: appendix.trim() });
115
+ }
116
+ return result;
117
+ } catch (err: any) {
118
+ const duration = Date.now() - start;
119
+ const report = classifyDuration(duration);
120
+ const appendix = formatTimingAppendix(report);
121
+
122
+ if (err?.message && typeof err.message === "string") {
123
+ err.message += appendix;
124
+ }
125
+ throw err;
126
+ }
127
+ },
128
+ };
129
+ }
package/src/types.ts CHANGED
@@ -18,6 +18,102 @@ export interface UsageStats {
18
18
  smoothedTps?: number;
19
19
  }
20
20
 
21
+ /** Structured file entry in a flow's output. */
22
+ export interface FileEntry {
23
+ /** Path to the file, relative or absolute. */
24
+ path: string;
25
+ /** Semantic role of this file in the flow's work. */
26
+ role?: "reference" | "read" | "modified" | "created" | "deleted" | "test";
27
+ /** Why this file matters (1 sentence). */
28
+ description?: string;
29
+ /** Short excerpt or snippet (not full content). */
30
+ snippet?: string;
31
+ /** Specific line ranges of interest. */
32
+ ranges?: Array<{
33
+ start: number;
34
+ end: number;
35
+ /** Free-form label like "bug", "fix", "ref", "added". */
36
+ label?: string;
37
+ }>;
38
+ }
39
+
40
+ /** Structured command/tool invocation entry in a flow's output. */
41
+ export interface CommandEntry {
42
+ /** The command string or tool call representation. */
43
+ command: string;
44
+ /** Tool used: bash, grep, find, ls, batch, read, write, edit, flow, web. */
45
+ tool?: string;
46
+ /** Working directory or target scope (file path, URL, etc.). */
47
+ target?: string;
48
+ /** Whether it succeeded, failed, or was aborted. */
49
+ result?: "success" | "failure" | "partial" | "aborted";
50
+ /** High-level output or error excerpt (not full stdout). */
51
+ output?: string;
52
+ /** Why this command was run (1 sentence). */
53
+ purpose?: string;
54
+ }
55
+
56
+ /** Compressed representation of a flow result for child context inheritance. */
57
+ export interface CompressedFlowResult {
58
+ /** Flow type (scout, build, debug, etc.). */
59
+ type: string;
60
+ /** Execution outcome. */
61
+ status: "accomplished" | "failed" | "aborted";
62
+ /** Files touched, read, or referenced. */
63
+ files?: FileEntry[];
64
+ /** Commands or tool calls executed. */
65
+ commands?: CommandEntry[];
66
+ /** Error message for failed/aborted flows. */
67
+ error?: string;
68
+ }
69
+
70
+ /** Action performed or attempted by a flow. */
71
+ export interface Action {
72
+ type: string;
73
+ description: string;
74
+ target?: string;
75
+ result?: "success" | "failure" | "partial" | "skipped";
76
+ evidence?: string;
77
+ }
78
+
79
+ /** Incomplete, skipped, blocked, or deferred work reported by a flow. */
80
+ export interface NotDoneItem {
81
+ /** The unfinished item. */
82
+ item: string;
83
+ /** Why the item was not completed. */
84
+ reason?: string;
85
+ /** Concrete blocker preventing completion, when applicable. */
86
+ blocker?: string;
87
+ /** Suggested follow-up for this item. */
88
+ nextStep?: string;
89
+ }
90
+
91
+ /** Structured JSON output from a flow run. */
92
+ export interface FlowStructuredOutput {
93
+ /** Schema version for forward compatibility. */
94
+ version: string;
95
+ /** Overall completion status. */
96
+ status: "complete" | "partial" | "blocked" | "failed";
97
+ /** 1–3 sentence summary of what was accomplished. */
98
+ summary: string;
99
+ /** Files touched, read, or referenced. */
100
+ files: FileEntry[];
101
+ /** Actions performed or attempted. */
102
+ actions: Action[];
103
+ /** Commands or tool calls executed during the flow. */
104
+ commands: CommandEntry[];
105
+ /** Incomplete, skipped, blocked, or deferred work. */
106
+ notDone: NotDoneItem[];
107
+ /** Recommended next steps or follow-up flows. */
108
+ nextSteps: string[];
109
+ /** Reasoning chains, hypotheses, inferences made during the flow. */
110
+ reasoning: string[];
111
+ /** Observations, warnings, caveats, side notes. */
112
+ notes: string[];
113
+ /** Escape hatch for flow-specific data (audit findings, debug root cause, etc.). */
114
+ extensions?: Record<string, unknown>;
115
+ }
116
+
21
117
  /** Result of a single flow invocation. */
22
118
  export interface SingleResult {
23
119
  type: string;
@@ -34,6 +130,8 @@ export interface SingleResult {
34
130
  sawAgentEnd?: boolean;
35
131
  /** Live in-progress text for status rendering; not part of the final flow report. */
36
132
  streamingText?: string;
133
+ /** Structured JSON output parsed from the flow's final response. */
134
+ structuredOutput?: FlowStructuredOutput;
37
135
  }
38
136
 
39
137
  /** Metadata attached to every tool result for rendering. */