pi-agent-flow 1.1.0 → 1.2.2

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.
@@ -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
+ }
@@ -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;
@@ -32,6 +128,10 @@ export interface SingleResult {
32
128
  stopReason?: string;
33
129
  errorMessage?: string;
34
130
  sawAgentEnd?: boolean;
131
+ /** Live in-progress text for status rendering; not part of the final flow report. */
132
+ streamingText?: string;
133
+ /** Structured JSON output parsed from the flow's final response. */
134
+ structuredOutput?: FlowStructuredOutput;
35
135
  }
36
136
 
37
137
  /** Metadata attached to every tool result for rendering. */
package/config.ts DELETED
@@ -1,102 +0,0 @@
1
- /**
2
- * Load flow tier model configuration from Pi settings files.
3
- *
4
- * Reads global (~/.pi/agent/settings.json) and project (.pi/settings.json)
5
- * settings, with project overriding global for flowModels.
6
- */
7
-
8
- import * as fs from "node:fs";
9
- import * as os from "node:os";
10
- import * as path from "node:path";
11
-
12
- export interface FlowModelConfig {
13
- lite?: string;
14
- flash?: string;
15
- full?: string;
16
- }
17
-
18
- export interface FlowSettings {
19
- toolOptimize?: boolean;
20
- }
21
-
22
- function readSettingsJson(filePath: string): Record<string, unknown> | null {
23
- try {
24
- const content = fs.readFileSync(filePath, "utf-8");
25
- return JSON.parse(content) as Record<string, unknown>;
26
- } catch {
27
- return null;
28
- }
29
- }
30
-
31
- function extractFlowModels(settings: Record<string, unknown> | null): FlowModelConfig {
32
- if (!settings) return {};
33
- const flowModels = settings.flowModels;
34
- if (!flowModels || typeof flowModels !== "object" || Array.isArray(flowModels)) {
35
- return {};
36
- }
37
- const obj = flowModels as Record<string, unknown>;
38
- const result: FlowModelConfig = {};
39
- for (const key of ["lite", "flash", "full"] as const) {
40
- if (typeof obj[key] === "string") {
41
- result[key] = obj[key] as string;
42
- }
43
- }
44
- return result;
45
- }
46
-
47
- function extractFlowSettings(settings: Record<string, unknown> | null): FlowSettings {
48
- if (!settings) return {};
49
- const flowSettings = settings.flowSettings;
50
- if (!flowSettings || typeof flowSettings !== "object" || Array.isArray(flowSettings)) {
51
- return {};
52
- }
53
- const obj = flowSettings as Record<string, unknown>;
54
- const result: FlowSettings = {};
55
- if (typeof obj.toolOptimize === "boolean") {
56
- result.toolOptimize = obj.toolOptimize;
57
- }
58
- return result;
59
- }
60
-
61
- function getGlobalSettingsPath(): string {
62
- const agentDir = process.env["PI_CODING_AGENT_DIR"]?.trim() || path.join(os.homedir(), ".pi", "agent");
63
- return path.join(agentDir, "settings.json");
64
- }
65
-
66
- function getProjectSettingsPath(cwd: string): string {
67
- return path.join(cwd, ".pi", "settings.json");
68
- }
69
-
70
- /**
71
- * Load flowModels from global and project settings.json.
72
- * Project overrides global (shallow merge per key).
73
- */
74
- export function loadFlowModels(cwd: string): FlowModelConfig {
75
- const globalSettings = readSettingsJson(getGlobalSettingsPath());
76
- const globalModels = extractFlowModels(globalSettings);
77
-
78
- const projectSettings = readSettingsJson(getProjectSettingsPath(cwd));
79
- const projectModels = extractFlowModels(projectSettings);
80
-
81
- return {
82
- ...globalModels,
83
- ...projectModels,
84
- };
85
- }
86
-
87
- /**
88
- * Load flowSettings from global and project settings.json.
89
- * Project overrides global (shallow merge per key).
90
- */
91
- export function loadFlowSettings(cwd: string): FlowSettings {
92
- const globalSettings = readSettingsJson(getGlobalSettingsPath());
93
- const globalFlowSettings = extractFlowSettings(globalSettings);
94
-
95
- const projectSettings = readSettingsJson(getProjectSettingsPath(cwd));
96
- const projectFlowSettings = extractFlowSettings(projectSettings);
97
-
98
- return {
99
- ...globalFlowSettings,
100
- ...projectFlowSettings,
101
- };
102
- }
package/runner-cli.js DELETED
@@ -1,254 +0,0 @@
1
- /**
2
- * Helpers for inheriting selected parent CLI flags in child flow processes.
3
- */
4
-
5
- import * as fs from "node:fs";
6
- import * as os from "node:os";
7
- import * as path from "node:path";
8
-
9
- function looksLikeExplicitRelativePath(value) {
10
- return (
11
- value.startsWith("./") ||
12
- value.startsWith("../") ||
13
- value.startsWith(".\\") ||
14
- value.startsWith("..\\")
15
- );
16
- }
17
-
18
- function resolvePathArg(value, options = {}) {
19
- const { allowPackageSource = false, alwaysResolveRelative = false } = options;
20
- if (!value) return value;
21
- if (allowPackageSource && (value.startsWith("npm:") || value.startsWith("git:"))) return value;
22
- if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
23
- if (path.isAbsolute(value)) return value;
24
-
25
- const resolved = path.resolve(process.cwd(), value);
26
- if (
27
- alwaysResolveRelative ||
28
- looksLikeExplicitRelativePath(value) ||
29
- path.extname(value) !== "" ||
30
- fs.existsSync(resolved)
31
- ) {
32
- return resolved;
33
- }
34
- return value;
35
- }
36
-
37
- /**
38
- * Parse process.argv into groups used for child flow invocations.
39
- *
40
- * - extensionArgs: forwarded with path resolution
41
- * - alwaysProxy: forwarded verbatim to every child
42
- * - fallbackModel/thinking/tools: used only when the flow file does not set them
43
- */
44
- export function parseFlowCliArgs(argv) {
45
- const extensionArgs = [];
46
- const alwaysProxy = [];
47
- let fallbackModel;
48
- let fallbackThinking;
49
- let fallbackTools;
50
- let fallbackNoTools = false;
51
- let tieredLiteModel;
52
- let tieredFlashModel;
53
- let tieredFullModel;
54
-
55
- let i = 2; // skip executable + script name
56
- while (i < argv.length) {
57
- const raw = argv[i];
58
- if (!raw.startsWith("-")) {
59
- i++;
60
- continue;
61
- }
62
-
63
- const eqIdx = raw.indexOf("=");
64
- const flagName = eqIdx !== -1 ? raw.slice(0, eqIdx) : raw;
65
- const inlineValue = eqIdx !== -1 ? raw.slice(eqIdx + 1) : undefined;
66
-
67
- const nextToken = argv[i + 1];
68
- const nextIsValue = nextToken !== undefined && !nextToken.startsWith("-");
69
-
70
- const getValue = () => {
71
- if (inlineValue !== undefined) return [inlineValue, 1];
72
- if (nextIsValue) return [nextToken, 2];
73
- return [undefined, 1];
74
- };
75
-
76
- if (
77
- [
78
- "--mode",
79
- "--session",
80
- "--append-system-prompt",
81
- "--export",
82
- "--flow-max-depth",
83
- ].includes(flagName)
84
- ) {
85
- const [, skip] = getValue();
86
- i += skip;
87
- continue;
88
- }
89
-
90
- if (["--flow-prevent-cycles", "--list-models"].includes(flagName)) {
91
- const [, skip] = getValue();
92
- i += skip;
93
- continue;
94
- }
95
-
96
- if (
97
- [
98
- "--print",
99
- "-p",
100
- "--no-session",
101
- "--continue",
102
- "-c",
103
- "--resume",
104
- "-r",
105
- "--offline",
106
- "--help",
107
- "-h",
108
- "--version",
109
- "-v",
110
- "--no-flow-prevent-cycles",
111
- ].includes(flagName)
112
- ) {
113
- i++;
114
- continue;
115
- }
116
-
117
- if (flagName === "--no-extensions" || flagName === "-ne") {
118
- extensionArgs.push(flagName);
119
- i++;
120
- continue;
121
- }
122
-
123
- if (flagName === "--extension" || flagName === "-e") {
124
- const [value, skip] = getValue();
125
- if (value !== undefined) {
126
- extensionArgs.push(flagName, resolvePathArg(value, { allowPackageSource: true }));
127
- }
128
- i += skip;
129
- continue;
130
- }
131
-
132
- if (["--skill", "--prompt-template", "--theme"].includes(flagName)) {
133
- const [value, skip] = getValue();
134
- if (value !== undefined) alwaysProxy.push(flagName, resolvePathArg(value));
135
- i += skip;
136
- continue;
137
- }
138
-
139
- if (flagName === "--session-dir") {
140
- const [value, skip] = getValue();
141
- if (value !== undefined) {
142
- alwaysProxy.push(flagName, resolvePathArg(value, { alwaysResolveRelative: true }));
143
- }
144
- i += skip;
145
- continue;
146
- }
147
-
148
- if (
149
- [
150
- "--provider",
151
- "--api-key",
152
- "--system-prompt",
153
- "--models",
154
- ].includes(flagName)
155
- ) {
156
- const [value, skip] = getValue();
157
- if (value !== undefined) alwaysProxy.push(flagName, value);
158
- i += skip;
159
- continue;
160
- }
161
-
162
- if (
163
- [
164
- "--no-skills",
165
- "-ns",
166
- "--no-prompt-templates",
167
- "-np",
168
- "--no-themes",
169
- "--verbose",
170
- ].includes(flagName)
171
- ) {
172
- alwaysProxy.push(flagName);
173
- i++;
174
- continue;
175
- }
176
-
177
- if (flagName === "--model") {
178
- const [value, skip] = getValue();
179
- if (value !== undefined) fallbackModel = value;
180
- i += skip;
181
- continue;
182
- }
183
-
184
- if (flagName === "--thinking") {
185
- const [value, skip] = getValue();
186
- if (value !== undefined) fallbackThinking = value;
187
- i += skip;
188
- continue;
189
- }
190
-
191
- if (flagName === "--tools") {
192
- const [value, skip] = getValue();
193
- if (value !== undefined) fallbackTools = value;
194
- i += skip;
195
- continue;
196
- }
197
-
198
- if (flagName === "--no-tools") {
199
- fallbackNoTools = true;
200
- i++;
201
- continue;
202
- }
203
-
204
- if (flagName === "--flow-lite-model") {
205
- const [value, skip] = getValue();
206
- if (value !== undefined) tieredLiteModel = value;
207
- i += skip;
208
- continue;
209
- }
210
-
211
- if (flagName === "--flow-flash-model") {
212
- const [value, skip] = getValue();
213
- if (value !== undefined) tieredFlashModel = value;
214
- i += skip;
215
- continue;
216
- }
217
-
218
- if (flagName === "--flow-full-model") {
219
- const [value, skip] = getValue();
220
- if (value !== undefined) tieredFullModel = value;
221
- i += skip;
222
- continue;
223
- }
224
-
225
- if (inlineValue !== undefined) {
226
- alwaysProxy.push(flagName, inlineValue);
227
- i++;
228
- continue;
229
- }
230
-
231
- if (nextIsValue) {
232
- alwaysProxy.push(flagName, nextToken);
233
- i += 2;
234
- continue;
235
- }
236
-
237
- alwaysProxy.push(flagName);
238
- i++;
239
- }
240
-
241
- return {
242
- extensionArgs,
243
- alwaysProxy,
244
- fallbackModel,
245
- fallbackThinking,
246
- fallbackTools,
247
- fallbackNoTools,
248
- tieredModels: {
249
- lite: tieredLiteModel,
250
- flash: tieredFlashModel,
251
- full: tieredFullModel,
252
- },
253
- };
254
- }