pi-agent-replicate 0.0.0-stage → 1.4.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.
Files changed (82) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +152 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +68 -2
  6. package/RELEASING.md +132 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +23 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/configuration.md +14 -0
  15. package/docs/development.md +27 -0
  16. package/docs/public-subagent-tree-api.md +58 -0
  17. package/docs/retained-roles.md +71 -0
  18. package/docs/settled-lifecycle-contract.md +71 -0
  19. package/docs/worktree-subagents.md +58 -0
  20. package/examples/role-pack/extension.ts +18 -0
  21. package/examples/role-pack/package.json +16 -0
  22. package/examples/role-pack/roles/example-reviewer.md +12 -0
  23. package/index.d.ts +116 -0
  24. package/index.js +1064 -0
  25. package/index.ts +155 -0
  26. package/package.json +70 -4
  27. package/pi-extension/subagents/activity.js +575 -0
  28. package/pi-extension/subagents/activity.ts +980 -0
  29. package/pi-extension/subagents/completion.js +184 -0
  30. package/pi-extension/subagents/completion.ts +229 -0
  31. package/pi-extension/subagents/delivery-drain.js +15 -0
  32. package/pi-extension/subagents/delivery-drain.ts +20 -0
  33. package/pi-extension/subagents/delivery-files.js +86 -0
  34. package/pi-extension/subagents/delivery-files.ts +105 -0
  35. package/pi-extension/subagents/delivery-log.js +51 -0
  36. package/pi-extension/subagents/delivery-log.ts +51 -0
  37. package/pi-extension/subagents/delivery-payload.ts +55 -0
  38. package/pi-extension/subagents/delivery.ts +219 -0
  39. package/pi-extension/subagents/execution-owner.js +30 -0
  40. package/pi-extension/subagents/execution-owner.ts +38 -0
  41. package/pi-extension/subagents/herdr.js +645 -0
  42. package/pi-extension/subagents/herdr.ts +927 -0
  43. package/pi-extension/subagents/index.ts +3400 -0
  44. package/pi-extension/subagents/launch.js +774 -0
  45. package/pi-extension/subagents/launch.ts +1124 -0
  46. package/pi-extension/subagents/lifecycle.js +477 -0
  47. package/pi-extension/subagents/lifecycle.ts +582 -0
  48. package/pi-extension/subagents/lineage.js +806 -0
  49. package/pi-extension/subagents/lineage.ts +841 -0
  50. package/pi-extension/subagents/replica-background.js +32 -0
  51. package/pi-extension/subagents/replica-background.ts +38 -0
  52. package/pi-extension/subagents/replica-branch.ts +45 -0
  53. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  54. package/pi-extension/subagents/replica-history.js +22 -0
  55. package/pi-extension/subagents/replica-history.ts +21 -0
  56. package/pi-extension/subagents/replica-work.js +326 -0
  57. package/pi-extension/subagents/replica-work.ts +336 -0
  58. package/pi-extension/subagents/replica-worker.js +74 -0
  59. package/pi-extension/subagents/replica-worker.ts +71 -0
  60. package/pi-extension/subagents/replication.ts +99 -0
  61. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  62. package/pi-extension/subagents/role-catalog.js +123 -0
  63. package/pi-extension/subagents/role-catalog.ts +24 -0
  64. package/pi-extension/subagents/runtime-routing.js +154 -0
  65. package/pi-extension/subagents/runtime-routing.ts +239 -0
  66. package/pi-extension/subagents/session-binding.js +346 -0
  67. package/pi-extension/subagents/session-binding.ts +339 -0
  68. package/pi-extension/subagents/session-work.js +219 -0
  69. package/pi-extension/subagents/session-work.ts +167 -0
  70. package/pi-extension/subagents/session.js +318 -0
  71. package/pi-extension/subagents/session.ts +448 -0
  72. package/pi-extension/subagents/settled-contract.js +47 -0
  73. package/pi-extension/subagents/settled-contract.ts +175 -0
  74. package/pi-extension/subagents/source-state.js +180 -0
  75. package/pi-extension/subagents/source-state.ts +184 -0
  76. package/pi-extension/subagents/status.ts +493 -0
  77. package/pi-extension/subagents/subagent-done.js +805 -0
  78. package/pi-extension/subagents/subagent-done.ts +878 -0
  79. package/pi-extension/subagents/terminal.js +111 -0
  80. package/pi-extension/subagents/terminal.ts +208 -0
  81. package/pi-extension/subagents/watchdog.js +122 -0
  82. package/pi-extension/subagents/watchdog.ts +171 -0
@@ -0,0 +1,219 @@
1
+ import { readFile } from "node:fs/promises";
2
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
3
+ /** Work receipts belong to this session, not inherited conversation or the terminal host. */
4
+ export class SessionWork {
5
+ tools = new Set();
6
+ terminals = new Set();
7
+ requests = new Set();
8
+ searches = new Set();
9
+ searchWaits = new Map();
10
+ completedTerminals = new Set();
11
+ completedRequests = new Set();
12
+ processes = new Map();
13
+ sessionId;
14
+ constructor(sessionId, saved) {
15
+ this.sessionId = sessionId;
16
+ if (saved?.sessionId !== sessionId)
17
+ return;
18
+ for (const source of saved.terminals ?? [])
19
+ if (typeof source === "string")
20
+ this.terminals.add(source);
21
+ for (const id of saved.requests ?? [])
22
+ if (typeof id === "string")
23
+ this.requests.add(id);
24
+ for (const id of saved.searches ?? [])
25
+ if (this.requests.has(id))
26
+ this.searches.add(id);
27
+ for (const identity of saved.processes ?? [])
28
+ if (Number.isInteger(identity.pid) && identity.pid > 0 && typeof identity.start === "string" && typeof identity.boot === "string")
29
+ this.processes.set(identity.pid, identity);
30
+ }
31
+ get pending() { return this.tools.size + this.terminals.size + this.requests.size + this.processes.size > 0; }
32
+ snapshot() { return { sessionId: this.sessionId, terminals: [...this.terminals], requests: [...this.requests], searches: [...this.searches].filter(id => this.requests.has(id)), processes: [...this.processes.values()] }; }
33
+ toolStarted(id, toolName) {
34
+ this.tools.add(id);
35
+ if (toolName === "wait_for_web_searches")
36
+ this.searchWaits.set(id, new Set(this.searches));
37
+ }
38
+ toolEnded(id) { this.tools.delete(id); this.searchWaits.delete(id); }
39
+ toolResult(value, inspection = false, toolName, toolCallId) {
40
+ const data = record(value);
41
+ if (inspection && data?.origin?.sessionId !== this.sessionId)
42
+ return;
43
+ if (!data || data.origin?.sessionId && data.origin.sessionId !== this.sessionId)
44
+ return;
45
+ // Deletion suppresses terminal-completion; it retires the receipt, not live PIDs.
46
+ if (toolName === "delete" && data.deleted === true && typeof data.source === "string" && data.source.startsWith("shell:") && (!data.snapshot?.origin?.sessionId || data.snapshot.origin.sessionId === this.sessionId)) {
47
+ this.completedTerminals.add(data.source);
48
+ this.terminals.delete(data.source);
49
+ }
50
+ // A successful wait drains its start-time search set, including searches
51
+ // cancelled by a provider retry. Other requests and later launches stay owned.
52
+ if (toolName === "wait_for_web_searches" && Array.isArray(data.results) && toolCallId) {
53
+ for (const id of this.searchWaits.get(toolCallId) ?? []) {
54
+ this.completedRequests.add(id);
55
+ this.requests.delete(id);
56
+ this.searches.delete(id);
57
+ }
58
+ }
59
+ if (typeof data.source === "string" && data.source.startsWith("shell:") && !this.completedTerminals.has(data.source) && ["running", "stopping"].includes(data.status))
60
+ this.terminals.add(data.source);
61
+ if (typeof data.requestId === "string") {
62
+ if (toolName === "web_search" && ["running", "queued"].includes(data.status) && !this.completedRequests.has(data.requestId))
63
+ this.searches.add(data.requestId);
64
+ if (["completed", "failed", "cancelled"].includes(data.status) || data.answers !== undefined) {
65
+ this.completedRequests.add(data.requestId);
66
+ this.requests.delete(data.requestId);
67
+ }
68
+ else if (!this.completedRequests.has(data.requestId) && ["running", "queued"].includes(data.status))
69
+ this.requests.add(data.requestId);
70
+ }
71
+ }
72
+ /** Completion arrival invalidates an old final answer; Pi processes the message normally. */
73
+ message(value) {
74
+ const message = record(value);
75
+ if (!message)
76
+ return false;
77
+ let changed = false;
78
+ if (message.customType === "terminal-completion") {
79
+ for (const session of message.details?.sessions ?? []) {
80
+ if (session.origin?.sessionId && session.origin.sessionId !== this.sessionId)
81
+ continue;
82
+ this.completedTerminals.add(session.source);
83
+ changed = this.terminals.delete(session.source) || changed;
84
+ }
85
+ }
86
+ const details = record(message.details);
87
+ if (details && typeof details.requestId === "string" && (message.customType === "ask:background-answer" || ["completed", "failed", "cancelled"].includes(details.status))) {
88
+ this.completedRequests.add(details.requestId);
89
+ changed = this.requests.delete(details.requestId) || changed;
90
+ }
91
+ return changed;
92
+ }
93
+ /** Track only reported native descendants. A remote PID is never local ownership. */
94
+ async trackProcesses(value) {
95
+ const data = record(value);
96
+ if (!data || data.remote || data.origin?.sessionId && data.origin.sessionId !== this.sessionId || !Number.isInteger(data.pid) || data.pid <= 0 || data.pid === process.pid)
97
+ return;
98
+ const identity = await processIdentity(data.pid);
99
+ if (!identity || !await descendsFrom(data.pid, process.pid))
100
+ return;
101
+ if (typeof data.source === "string" && data.source.startsWith("shell:"))
102
+ identity.terminal = data.source;
103
+ this.processes.set(identity.pid, identity);
104
+ await this.captureChildren(identity);
105
+ }
106
+ async captureChildren(identity) {
107
+ const current = await processIdentity(identity.pid);
108
+ if (!sameProcess(identity, current))
109
+ return;
110
+ let children;
111
+ try {
112
+ children = await readFile(`/proc/${identity.pid}/task/${identity.pid}/children`, "utf8");
113
+ }
114
+ catch {
115
+ return;
116
+ }
117
+ for (const pid of children.trim().split(/\s+/).map(Number).filter(pid => pid > 0)) {
118
+ if (this.processes.has(pid))
119
+ continue;
120
+ const child = await processIdentity(pid);
121
+ if (child) {
122
+ child.terminal = identity.terminal;
123
+ this.processes.set(pid, child);
124
+ await this.captureChildren(child);
125
+ }
126
+ }
127
+ }
128
+ /** Keep unknown ownership pending; a vanished or replaced incarnation is finished. */
129
+ async refreshProcesses() {
130
+ const completed = [];
131
+ if (!this.processes.size)
132
+ return completed;
133
+ let boot;
134
+ try {
135
+ boot = (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim();
136
+ }
137
+ catch {
138
+ return completed;
139
+ }
140
+ for (const identity of this.processes.values()) {
141
+ // A different machine/boot cannot tell us whether the original job ended.
142
+ if (identity.boot !== boot)
143
+ continue;
144
+ await this.captureChildren(identity);
145
+ const current = await processIdentity(identity.pid);
146
+ if (current === undefined)
147
+ continue;
148
+ if (sameProcess(identity, current)) {
149
+ // Terminal completion already reports its shell and commands. Wake again only
150
+ // for a descendant observed still alive after that completion was delivered.
151
+ if (identity.terminal && !this.terminals.has(identity.terminal))
152
+ identity.notify = true;
153
+ }
154
+ else {
155
+ this.processes.delete(identity.pid);
156
+ if (!identity.terminal || identity.notify)
157
+ completed.push(identity.pid);
158
+ }
159
+ }
160
+ return completed;
161
+ }
162
+ }
163
+ function sameProcess(a, b) { return !!b && a.start === b.start && a.boot === b.boot; }
164
+ async function processFields(pid) {
165
+ try {
166
+ const stat = await readFile(`/proc/${pid}/stat`, "utf8");
167
+ return stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
168
+ }
169
+ catch (error) {
170
+ return error.code === "ENOENT" ? null : undefined;
171
+ }
172
+ }
173
+ async function processIdentity(pid) {
174
+ if (process.platform !== "linux")
175
+ return undefined;
176
+ const fields = await processFields(pid);
177
+ if (!fields)
178
+ return fields;
179
+ if (["Z", "X"].includes(fields[0]))
180
+ return null;
181
+ try {
182
+ return { pid, start: fields[19], boot: (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim() };
183
+ }
184
+ catch {
185
+ return undefined;
186
+ }
187
+ }
188
+ async function descendsFrom(pid, owner) {
189
+ const seen = new Set();
190
+ while (pid > 1 && !seen.has(pid)) {
191
+ seen.add(pid);
192
+ const fields = await processFields(pid);
193
+ if (!fields)
194
+ return false;
195
+ pid = Number(fields[1]);
196
+ if (pid === owner)
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+ /** Tool wrappers may expose data in details, structuredContent, or a JSON receipt. */
202
+ export function workFacts(result) {
203
+ const value = record(result);
204
+ if (!value)
205
+ return [];
206
+ const facts = [value.details, value.structuredContent];
207
+ for (const part of value.content ?? []) {
208
+ if (part?.type !== "text" || typeof part.text !== "string" || !part.text.trimStart().startsWith("{"))
209
+ continue;
210
+ try {
211
+ facts.push(JSON.parse(part.text));
212
+ }
213
+ catch { /* Ordinary text is not a receipt. */ }
214
+ }
215
+ return facts.filter(Boolean).flatMap(fact => {
216
+ const results = record(fact)?.results;
217
+ return Array.isArray(results) ? [fact, ...results] : [fact];
218
+ });
219
+ }
@@ -0,0 +1,167 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ interface OwnedProcess { pid: number; start: string; boot: string; terminal?: string; notify?: boolean }
4
+ export interface SessionWorkSnapshot {
5
+ sessionId: string;
6
+ terminals: string[];
7
+ requests: string[];
8
+ searches?: string[];
9
+ processes: OwnedProcess[];
10
+ }
11
+ const record = (value: unknown): Record<string, any> | undefined => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, any> : undefined;
12
+
13
+ /** Work receipts belong to this session, not inherited conversation or the terminal host. */
14
+ export class SessionWork {
15
+ private readonly tools = new Set<string>();
16
+ private readonly terminals = new Set<string>();
17
+ private readonly requests = new Set<string>();
18
+ private readonly searches = new Set<string>();
19
+ private readonly searchWaits = new Map<string, Set<string>>();
20
+ private readonly completedTerminals = new Set<string>();
21
+ private readonly completedRequests = new Set<string>();
22
+ private readonly processes = new Map<number, OwnedProcess>();
23
+ private readonly sessionId: string;
24
+ constructor(sessionId: string, saved?: SessionWorkSnapshot) {
25
+ this.sessionId = sessionId;
26
+ if (saved?.sessionId !== sessionId) return;
27
+ for (const source of saved.terminals ?? []) if (typeof source === "string") this.terminals.add(source);
28
+ for (const id of saved.requests ?? []) if (typeof id === "string") this.requests.add(id);
29
+ for (const id of saved.searches ?? []) if (this.requests.has(id)) this.searches.add(id);
30
+ for (const identity of saved.processes ?? []) if (Number.isInteger(identity.pid) && identity.pid > 0 && typeof identity.start === "string" && typeof identity.boot === "string") this.processes.set(identity.pid, identity);
31
+ }
32
+ get pending(): boolean { return this.tools.size + this.terminals.size + this.requests.size + this.processes.size > 0; }
33
+ snapshot(): SessionWorkSnapshot { return { sessionId: this.sessionId, terminals: [...this.terminals], requests: [...this.requests], searches: [...this.searches].filter(id => this.requests.has(id)), processes: [...this.processes.values()] }; }
34
+ toolStarted(id: string, toolName?: string): void {
35
+ this.tools.add(id);
36
+ if (toolName === "wait_for_web_searches") this.searchWaits.set(id, new Set(this.searches));
37
+ }
38
+ toolEnded(id: string): void { this.tools.delete(id); this.searchWaits.delete(id); }
39
+ toolResult(value: unknown, inspection = false, toolName?: string, toolCallId?: string): void {
40
+ const data = record(value);
41
+ if (inspection && data?.origin?.sessionId !== this.sessionId) return;
42
+ if (!data || data.origin?.sessionId && data.origin.sessionId !== this.sessionId) return;
43
+ // Deletion suppresses terminal-completion; it retires the receipt, not live PIDs.
44
+ if (toolName === "delete" && data.deleted === true && typeof data.source === "string" && data.source.startsWith("shell:") && (!data.snapshot?.origin?.sessionId || data.snapshot.origin.sessionId === this.sessionId)) {
45
+ this.completedTerminals.add(data.source);
46
+ this.terminals.delete(data.source);
47
+ }
48
+ // A successful wait drains its start-time search set, including searches
49
+ // cancelled by a provider retry. Other requests and later launches stay owned.
50
+ if (toolName === "wait_for_web_searches" && Array.isArray(data.results) && toolCallId) {
51
+ for (const id of this.searchWaits.get(toolCallId) ?? []) {
52
+ this.completedRequests.add(id);
53
+ this.requests.delete(id);
54
+ this.searches.delete(id);
55
+ }
56
+ }
57
+ if (typeof data.source === "string" && data.source.startsWith("shell:") && !this.completedTerminals.has(data.source) && ["running", "stopping"].includes(data.status)) this.terminals.add(data.source);
58
+ if (typeof data.requestId === "string") {
59
+ if (toolName === "web_search" && ["running", "queued"].includes(data.status) && !this.completedRequests.has(data.requestId)) this.searches.add(data.requestId);
60
+ if (["completed", "failed", "cancelled"].includes(data.status) || data.answers !== undefined) {
61
+ this.completedRequests.add(data.requestId);
62
+ this.requests.delete(data.requestId);
63
+ } else if (!this.completedRequests.has(data.requestId) && ["running", "queued"].includes(data.status)) this.requests.add(data.requestId);
64
+ }
65
+ }
66
+ /** Completion arrival invalidates an old final answer; Pi processes the message normally. */
67
+ message(value: unknown): boolean {
68
+ const message = record(value);
69
+ if (!message) return false;
70
+ let changed = false;
71
+ if (message.customType === "terminal-completion") {
72
+ for (const session of message.details?.sessions ?? []) {
73
+ if (session.origin?.sessionId && session.origin.sessionId !== this.sessionId) continue;
74
+ this.completedTerminals.add(session.source);
75
+ changed = this.terminals.delete(session.source) || changed;
76
+ }
77
+ }
78
+ const details = record(message.details);
79
+ if (details && typeof details.requestId === "string" && (message.customType === "ask:background-answer" || ["completed", "failed", "cancelled"].includes(details.status))) {
80
+ this.completedRequests.add(details.requestId);
81
+ changed = this.requests.delete(details.requestId) || changed;
82
+ }
83
+ return changed;
84
+ }
85
+ /** Track only reported native descendants. A remote PID is never local ownership. */
86
+ async trackProcesses(value: unknown): Promise<void> {
87
+ const data = record(value);
88
+ if (!data || data.remote || data.origin?.sessionId && data.origin.sessionId !== this.sessionId || !Number.isInteger(data.pid) || data.pid <= 0 || data.pid === process.pid) return;
89
+ const identity = await processIdentity(data.pid);
90
+ if (!identity || !await descendsFrom(data.pid, process.pid)) return;
91
+ if (typeof data.source === "string" && data.source.startsWith("shell:")) identity.terminal = data.source;
92
+ this.processes.set(identity.pid, identity);
93
+ await this.captureChildren(identity);
94
+ }
95
+ private async captureChildren(identity: OwnedProcess): Promise<void> {
96
+ const current = await processIdentity(identity.pid);
97
+ if (!sameProcess(identity, current)) return;
98
+ let children: string;
99
+ try { children = await readFile(`/proc/${identity.pid}/task/${identity.pid}/children`, "utf8"); } catch { return; }
100
+ for (const pid of children.trim().split(/\s+/).map(Number).filter(pid => pid > 0)) {
101
+ if (this.processes.has(pid)) continue;
102
+ const child = await processIdentity(pid);
103
+ if (child) { child.terminal = identity.terminal; this.processes.set(pid, child); await this.captureChildren(child); }
104
+ }
105
+ }
106
+ /** Keep unknown ownership pending; a vanished or replaced incarnation is finished. */
107
+ async refreshProcesses(): Promise<number[]> {
108
+ const completed: number[] = [];
109
+ if (!this.processes.size) return completed;
110
+ let boot: string;
111
+ try { boot = (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim(); } catch { return completed; }
112
+ for (const identity of this.processes.values()) {
113
+ // A different machine/boot cannot tell us whether the original job ended.
114
+ if (identity.boot !== boot) continue;
115
+ await this.captureChildren(identity);
116
+ const current = await processIdentity(identity.pid);
117
+ if (current === undefined) continue;
118
+ if (sameProcess(identity, current)) {
119
+ // Terminal completion already reports its shell and commands. Wake again only
120
+ // for a descendant observed still alive after that completion was delivered.
121
+ if (identity.terminal && !this.terminals.has(identity.terminal)) identity.notify = true;
122
+ } else {
123
+ this.processes.delete(identity.pid);
124
+ if (!identity.terminal || identity.notify) completed.push(identity.pid);
125
+ }
126
+ }
127
+ return completed;
128
+ }
129
+ }
130
+ function sameProcess(a: OwnedProcess, b: OwnedProcess | null | undefined): boolean { return !!b && a.start === b.start && a.boot === b.boot; }
131
+ async function processFields(pid: number): Promise<string[] | null | undefined> {
132
+ try { const stat = await readFile(`/proc/${pid}/stat`, "utf8"); return stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/); }
133
+ catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? null : undefined; }
134
+ }
135
+ async function processIdentity(pid: number): Promise<OwnedProcess | null | undefined> {
136
+ if (process.platform !== "linux") return undefined;
137
+ const fields = await processFields(pid);
138
+ if (!fields) return fields;
139
+ if (["Z", "X"].includes(fields[0])) return null;
140
+ try { return { pid, start: fields[19], boot: (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim() }; } catch { return undefined; }
141
+ }
142
+ async function descendsFrom(pid: number, owner: number): Promise<boolean> {
143
+ const seen = new Set<number>();
144
+ while (pid > 1 && !seen.has(pid)) {
145
+ seen.add(pid);
146
+ const fields = await processFields(pid);
147
+ if (!fields) return false;
148
+ pid = Number(fields[1]);
149
+ if (pid === owner) return true;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ /** Tool wrappers may expose data in details, structuredContent, or a JSON receipt. */
155
+ export function workFacts(result: unknown): unknown[] {
156
+ const value = record(result);
157
+ if (!value) return [];
158
+ const facts: unknown[] = [value.details, value.structuredContent];
159
+ for (const part of value.content ?? []) {
160
+ if (part?.type !== "text" || typeof part.text !== "string" || !part.text.trimStart().startsWith("{")) continue;
161
+ try { facts.push(JSON.parse(part.text)); } catch { /* Ordinary text is not a receipt. */ }
162
+ }
163
+ return facts.filter(Boolean).flatMap(fact => {
164
+ const results = record(fact)?.results;
165
+ return Array.isArray(results) ? [fact, ...results] : [fact];
166
+ });
167
+ }