pi-agent-flow 0.1.0-alpha.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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Helpers for parsing Pi JSON mode events and summarizing flow results.
3
+ */
4
+
5
+ function getSeenFlowMessageSignatures(result) {
6
+ if (!Object.prototype.hasOwnProperty.call(result, "__seenMessageSignatures")) {
7
+ Object.defineProperty(result, "__seenMessageSignatures", {
8
+ value: new Set(),
9
+ enumerable: false,
10
+ configurable: false,
11
+ writable: false,
12
+ });
13
+ }
14
+ return result.__seenMessageSignatures;
15
+ }
16
+
17
+ function stableStringify(value) {
18
+ if (value === null || typeof value !== "object") {
19
+ return JSON.stringify(value);
20
+ }
21
+
22
+ if (Array.isArray(value)) {
23
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
24
+ }
25
+
26
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
27
+ return `{${entries
28
+ .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)
29
+ .join(",")}}`;
30
+ }
31
+
32
+ function getMessageSignature(message) {
33
+ return stableStringify(message);
34
+ }
35
+
36
+ function updateAssistantMetadata(result, message) {
37
+ if (!message || message.role !== "assistant") return;
38
+ if (!result.model && message.model) result.model = message.model;
39
+ if (message.stopReason) result.stopReason = message.stopReason;
40
+ if (message.errorMessage) result.errorMessage = message.errorMessage;
41
+ }
42
+
43
+ function addFlowAssistantMessage(result, message) {
44
+ if (!message || message.role !== "assistant") return false;
45
+
46
+ updateAssistantMetadata(result, message);
47
+
48
+ const signature = getMessageSignature(message);
49
+ const seen = getSeenFlowMessageSignatures(result);
50
+ if (seen.has(signature)) return false;
51
+ seen.add(signature);
52
+
53
+ result.messages.push(message);
54
+
55
+ result.usage.turns++;
56
+ const usage = message.usage;
57
+ if (usage) {
58
+ result.usage.input += usage.input || 0;
59
+ result.usage.output += usage.output || 0;
60
+ result.usage.cacheRead += usage.cacheRead || 0;
61
+ result.usage.cacheWrite += usage.cacheWrite || 0;
62
+ result.usage.cost += usage.cost?.total || 0;
63
+ result.usage.contextTokens = usage.totalTokens || 0;
64
+ }
65
+
66
+ // Count tool call parts in the message content
67
+ if (Array.isArray(message.content)) {
68
+ for (const part of message.content) {
69
+ if (part.type === "toolCall") {
70
+ result.usage.toolCalls++;
71
+ }
72
+ }
73
+ }
74
+
75
+ return true;
76
+ }
77
+
78
+ function addFlowAssistantMessages(result, messages) {
79
+ if (!Array.isArray(messages)) return false;
80
+ let changed = false;
81
+ for (const message of messages) {
82
+ if (addFlowAssistantMessage(result, message)) changed = true;
83
+ }
84
+ return changed;
85
+ }
86
+
87
+ export function processFlowEvent(event, result) {
88
+ if (!event || typeof event !== "object") return false;
89
+
90
+ switch (event.type) {
91
+ case "message_end":
92
+ return addFlowAssistantMessage(result, event.message);
93
+
94
+ case "turn_end":
95
+ return addFlowAssistantMessage(result, event.message);
96
+
97
+ case "agent_end":
98
+ result.sawAgentEnd = true;
99
+ return addFlowAssistantMessages(result, event.messages);
100
+
101
+ default:
102
+ return false;
103
+ }
104
+ }
105
+
106
+ export function processFlowJsonLine(line, result) {
107
+ if (!line.trim()) return false;
108
+
109
+ let event;
110
+ try {
111
+ event = JSON.parse(line);
112
+ } catch {
113
+ return false;
114
+ }
115
+
116
+ return processFlowEvent(event, result);
117
+ }
118
+
119
+ export function getFlowFinalText(messages) {
120
+ if (!Array.isArray(messages)) return "";
121
+
122
+ for (let i = messages.length - 1; i >= 0; i--) {
123
+ const message = messages[i];
124
+ if (!message || message.role !== "assistant" || !Array.isArray(message.content)) {
125
+ continue;
126
+ }
127
+
128
+ for (const part of message.content) {
129
+ if (part?.type === "text" && typeof part.text === "string" && part.text.length > 0) {
130
+ return part.text;
131
+ }
132
+ }
133
+ }
134
+
135
+ return "";
136
+ }
137
+
138
+ export function getFlowSummaryText(result) {
139
+ const finalText = getFlowFinalText(result?.messages);
140
+ if (finalText) return finalText;
141
+
142
+ if (typeof result?.errorMessage === "string" && result.errorMessage.trim()) {
143
+ return result.errorMessage.trim();
144
+ }
145
+
146
+ const isError =
147
+ (typeof result?.exitCode === "number" && result.exitCode > 0) ||
148
+ result?.stopReason === "error" ||
149
+ result?.stopReason === "aborted";
150
+
151
+ if (isError && typeof result?.stderr === "string" && result.stderr.trim()) {
152
+ return result.stderr.trim();
153
+ }
154
+
155
+ return "(no output)";
156
+ }
package/types.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Shared type definitions for the flow-state delegation extension.
3
+ */
4
+
5
+ import type { Message } from "@mariozechner/pi-ai";
6
+ import { getFlowFinalText } from "./runner-events.js";
7
+
8
+ /** Aggregated token usage from a flow run. */
9
+ export interface UsageStats {
10
+ input: number;
11
+ output: number;
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ cost: number;
15
+ contextTokens: number;
16
+ turns: number;
17
+ toolCalls: number;
18
+ }
19
+
20
+ /** Result of a single flow invocation. */
21
+ export interface SingleResult {
22
+ type: string;
23
+ agentSource: "user" | "project" | "unknown";
24
+ intent: string;
25
+ exitCode: number;
26
+ messages: Message[];
27
+ stderr: string;
28
+ usage: UsageStats;
29
+ model?: string;
30
+ stopReason?: string;
31
+ errorMessage?: string;
32
+ sawAgentEnd?: boolean;
33
+ }
34
+
35
+ /** Metadata attached to every tool result for rendering. */
36
+ export interface FlowDetails {
37
+ mode: "flow";
38
+ delegationMode: "fork";
39
+ projectAgentsDir: string | null;
40
+ results: SingleResult[];
41
+ }
42
+
43
+ /** A display-friendly representation of a message part. */
44
+ export type DisplayItem =
45
+ | { type: "text"; text: string }
46
+ | { type: "toolCall"; name: string; args: Record<string, unknown> };
47
+
48
+ /** Create an empty UsageStats object. */
49
+ export function emptyFlowUsage(): UsageStats {
50
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0, toolCalls: 0 };
51
+ }
52
+
53
+ /** Sum usage across multiple results. */
54
+ export function aggregateFlowUsage(results: SingleResult[]): UsageStats {
55
+ const total = emptyFlowUsage();
56
+ for (const r of results) {
57
+ total.input += r.usage.input;
58
+ total.output += r.usage.output;
59
+ total.cacheRead += r.usage.cacheRead;
60
+ total.cacheWrite += r.usage.cacheWrite;
61
+ total.cost += r.usage.cost;
62
+ total.turns += r.usage.turns;
63
+ total.toolCalls += r.usage.toolCalls;
64
+ }
65
+ return total;
66
+ }
67
+
68
+ /** Whether the child emitted a final assistant text response. */
69
+ export function hasFlowOutput(r: Pick<SingleResult, "messages">): boolean {
70
+ return getFlowFinalText(r.messages).trim().length > 0;
71
+ }
72
+
73
+ /** Whether the child semantically completed the run. */
74
+ export function isFlowComplete(r: Pick<SingleResult, "messages" | "sawAgentEnd">): boolean {
75
+ return Boolean(r.sawAgentEnd) && hasFlowOutput(r);
76
+ }
77
+
78
+ /** Whether a result should be treated as successful by the wrapper/UI. */
79
+ export function isFlowSuccess(r: SingleResult): boolean {
80
+ if (r.exitCode === -1) return false;
81
+ if (isFlowComplete(r)) return true;
82
+ return r.exitCode === 0 && r.stopReason !== "error" && r.stopReason !== "aborted";
83
+ }
84
+
85
+ /** Whether a result represents an error. */
86
+ export function isFlowError(r: SingleResult): boolean {
87
+ if (r.exitCode === -1) return false;
88
+ return !isFlowSuccess(r);
89
+ }
90
+
91
+ /** Reconcile process exit status with semantic completion observed from Pi's event stream. */
92
+ export function normalizeFlowResult(result: SingleResult, wasAborted: boolean): SingleResult {
93
+ const hasSemanticSuccess = isFlowComplete(result);
94
+
95
+ if (wasAborted) {
96
+ if (hasSemanticSuccess) {
97
+ result.exitCode = 0;
98
+ if (result.stopReason === "aborted") result.stopReason = undefined;
99
+ if (result.errorMessage === "Flow was aborted.") {
100
+ result.errorMessage = undefined;
101
+ }
102
+ } else {
103
+ result.exitCode = 130;
104
+ result.stopReason = "aborted";
105
+ result.errorMessage = "Flow was aborted.";
106
+ if (!result.stderr.trim()) result.stderr = "Flow was aborted.";
107
+ }
108
+ return result;
109
+ }
110
+
111
+ if (result.exitCode > 0) {
112
+ if (hasSemanticSuccess) {
113
+ result.exitCode = 0;
114
+ if (result.stopReason === "error") result.stopReason = undefined;
115
+ if (result.errorMessage === result.stderr.trim()) {
116
+ result.errorMessage = undefined;
117
+ }
118
+ } else {
119
+ if (!result.stopReason) result.stopReason = "error";
120
+ if (!result.errorMessage && result.stderr.trim()) {
121
+ result.errorMessage = result.stderr.trim();
122
+ }
123
+ }
124
+ }
125
+
126
+ return result;
127
+ }
128
+
129
+ /** Extract the last assistant text from a message history. */
130
+ export function getFlowOutput(messages: Message[]): string {
131
+ return getFlowFinalText(messages);
132
+ }
133
+
134
+ /** Extract all display-worthy items from a message history. */
135
+ export function getFlowDisplayItems(messages: Message[]): DisplayItem[] {
136
+ const items: DisplayItem[] = [];
137
+ for (const msg of messages) {
138
+ if (msg.role === "assistant") {
139
+ for (const part of msg.content) {
140
+ if (part.type === "text") {
141
+ items.push({ type: "text", text: part.text });
142
+ } else if (part.type === "toolCall") {
143
+ items.push({ type: "toolCall", name: part.name, args: part.arguments });
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return items;
149
+ }