pi-blackhole 0.4.2 → 0.4.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/README.md +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
* and throws if the API errored without collecting any tool results.
|
|
7
7
|
* This allows the consolidation pipeline to fall back to alternative models.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
agentLoop,
|
|
11
|
+
type AgentContext,
|
|
12
|
+
type AgentLoopConfig,
|
|
13
|
+
type AgentTool,
|
|
14
|
+
} from "@earendil-works/pi-agent-core";
|
|
10
15
|
import type { Message, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
11
16
|
import { createBridgeStreamFn } from "../../provider-stream.js";
|
|
12
17
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
@@ -20,166 +25,191 @@ import type { Observation, Relevance } from "../../ledger/index.js";
|
|
|
20
25
|
import { estimateStringTokens } from "../../tokens.js";
|
|
21
26
|
|
|
22
27
|
interface RunObserverArgs {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
model: Model<any>;
|
|
29
|
+
apiKey: string;
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
priorReflections: string[];
|
|
32
|
+
priorObservations: string[];
|
|
33
|
+
chunk: string;
|
|
34
|
+
allowedSourceEntryIds: string[];
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
agentLoop?: typeof agentLoop;
|
|
37
|
+
/** Optional custom stream function bypassing agentLoop's default streamSimple.
|
|
38
|
+
* Used by the Symbol.for bridge to access native pi-ai provider registrations
|
|
39
|
+
* from jiti-loaded consolidation agents. */
|
|
40
|
+
streamFn?: (model: any, context: any, options: any) => any;
|
|
41
|
+
maxTurns?: number;
|
|
42
|
+
thinkingLevel?: ModelThinkingLevel;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
const RelevanceSchema = Type.Union([
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
Type.Literal("low"),
|
|
47
|
+
Type.Literal("medium"),
|
|
48
|
+
Type.Literal("high"),
|
|
49
|
+
Type.Literal("critical"),
|
|
45
50
|
]);
|
|
46
51
|
|
|
47
|
-
export const OBSERVATION_TIMESTAMP_PATTERN =
|
|
52
|
+
export const OBSERVATION_TIMESTAMP_PATTERN =
|
|
53
|
+
"^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$";
|
|
48
54
|
|
|
49
55
|
const RecordObservationsSchema = Type.Object({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
observations: Type.Array(
|
|
57
|
+
Type.Object({
|
|
58
|
+
timestamp: Type.String({
|
|
59
|
+
pattern: OBSERVATION_TIMESTAMP_PATTERN,
|
|
60
|
+
description: "Observation time in local 'YYYY-MM-DD HH:MM' format.",
|
|
61
|
+
}),
|
|
62
|
+
content: Type.String({
|
|
63
|
+
minLength: 1,
|
|
64
|
+
description:
|
|
65
|
+
"Single-line plain prose. No markdown, no tags, no embedded timestamp.",
|
|
66
|
+
}),
|
|
67
|
+
relevance: RelevanceSchema,
|
|
68
|
+
sourceEntryIds: Type.Array(Type.String({ minLength: 1 }), {
|
|
69
|
+
minItems: 1,
|
|
70
|
+
description:
|
|
71
|
+
"Exact source entry ids from the chunk that directly support this observation. " +
|
|
72
|
+
"Use only ids shown in '[Source entry id: ...]' labels; never invent ids.",
|
|
73
|
+
}),
|
|
74
|
+
}),
|
|
75
|
+
{
|
|
76
|
+
description:
|
|
77
|
+
"Batch of new observations. May be empty only if the tool is not called at all.",
|
|
78
|
+
},
|
|
79
|
+
),
|
|
73
80
|
});
|
|
74
81
|
|
|
75
82
|
type RecordObservationsArgs = Static<typeof RecordObservationsSchema>;
|
|
76
83
|
|
|
77
84
|
function joinOrEmpty(items: string[]): string {
|
|
78
|
-
|
|
85
|
+
return items.length ? items.join("\n") : "(none yet)";
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
export function normalizeSourceEntryIds(
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
sourceEntryIds: readonly string[] | undefined,
|
|
90
|
+
allowedSourceEntryIds: readonly string[],
|
|
84
91
|
): string[] | undefined {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
if (!sourceEntryIds || sourceEntryIds.length === 0) return undefined;
|
|
93
|
+
const allowedOrder = new Map<string, number>();
|
|
94
|
+
for (let i = 0; i < allowedSourceEntryIds.length; i++)
|
|
95
|
+
allowedOrder.set(allowedSourceEntryIds[i], i);
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
// Filter out invalid/unknown IDs instead of rejecting the entire batch.
|
|
98
|
+
// Matches the dropper's normalizeDropObservationIds pattern: one hallucinated
|
|
99
|
+
// ID from the LLM should not discard valid observations.
|
|
100
|
+
const seen = new Set<string>();
|
|
101
|
+
const valid: string[] = [];
|
|
102
|
+
for (const id of sourceEntryIds) {
|
|
103
|
+
if (!allowedOrder.has(id)) continue;
|
|
104
|
+
if (seen.has(id)) continue;
|
|
105
|
+
seen.add(id);
|
|
106
|
+
valid.push(id);
|
|
107
|
+
}
|
|
108
|
+
if (valid.length === 0) return undefined;
|
|
109
|
+
return valid.sort(
|
|
110
|
+
(a, b) => (allowedOrder.get(a) ?? 0) - (allowedOrder.get(b) ?? 0),
|
|
111
|
+
);
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
/** Result returned by runObserver when no observations are recorded. */
|
|
105
115
|
export type ObserverEmptyReason =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
| { kind: "no_new_content" } // model ran but nothing worth recording
|
|
117
|
+
| { kind: "tool_not_called" } // model didn't call record_observations at all
|
|
118
|
+
| { kind: "all_rejected"; count: number } // tool called but all sourceEntryIds invalid
|
|
119
|
+
| { kind: "all_duplicates"; count: number } // tool called but all already seen
|
|
120
|
+
| { kind: "empty_array"; count: number }; // tool called but returned empty observations array
|
|
111
121
|
|
|
112
122
|
export interface ObserverResult {
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
observations: Observation[] | undefined;
|
|
124
|
+
emptyReason?: ObserverEmptyReason;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
export async function runObserver(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
export async function runObserver(
|
|
128
|
+
args: RunObserverArgs,
|
|
129
|
+
): Promise<ObserverResult> {
|
|
130
|
+
const {
|
|
131
|
+
model,
|
|
132
|
+
apiKey,
|
|
133
|
+
headers,
|
|
134
|
+
priorReflections,
|
|
135
|
+
priorObservations,
|
|
136
|
+
chunk,
|
|
137
|
+
allowedSourceEntryIds,
|
|
138
|
+
signal,
|
|
139
|
+
} = args;
|
|
140
|
+
const conversation = chunk.trim();
|
|
141
|
+
if (!conversation) return { observations: undefined };
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
const accumulated = new Map<string, Observation>();
|
|
144
|
+
let toolCalled = false;
|
|
145
|
+
let totalAdded = 0;
|
|
146
|
+
let totalDuplicates = 0;
|
|
147
|
+
let totalRejected = 0;
|
|
148
|
+
let totalProposed = 0;
|
|
128
149
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
150
|
+
const recordObservations: AgentTool<typeof RecordObservationsSchema> = {
|
|
151
|
+
name: "record_observations",
|
|
152
|
+
label: "Record observations",
|
|
153
|
+
description:
|
|
154
|
+
"Record a batch of new observations distilled from the conversation chunk. " +
|
|
155
|
+
"Call this multiple times as you work through the chunk. Stop calling when coverage is complete, " +
|
|
156
|
+
"then emit a short plain-text confirmation to end the run.",
|
|
157
|
+
parameters: RecordObservationsSchema,
|
|
158
|
+
execute: async (_id, params: RecordObservationsArgs) => {
|
|
159
|
+
toolCalled = true;
|
|
160
|
+
let added = 0;
|
|
161
|
+
let duplicates = 0;
|
|
162
|
+
let rejected = 0;
|
|
163
|
+
for (const obs of params.observations) {
|
|
164
|
+
totalProposed++;
|
|
165
|
+
const sourceEntryIds = normalizeSourceEntryIds(
|
|
166
|
+
obs.sourceEntryIds,
|
|
167
|
+
allowedSourceEntryIds,
|
|
168
|
+
);
|
|
169
|
+
if (!sourceEntryIds) {
|
|
170
|
+
rejected++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const content = truncateRecordContent(obs.content);
|
|
174
|
+
const id = hashId(content);
|
|
175
|
+
if (accumulated.has(id)) {
|
|
176
|
+
duplicates++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
accumulated.set(id, {
|
|
180
|
+
id,
|
|
181
|
+
content,
|
|
182
|
+
timestamp: obs.timestamp,
|
|
183
|
+
relevance: obs.relevance as Relevance,
|
|
184
|
+
sourceEntryIds,
|
|
185
|
+
tokenCount: estimateStringTokens(content),
|
|
186
|
+
});
|
|
187
|
+
added++;
|
|
188
|
+
}
|
|
189
|
+
totalAdded += added;
|
|
190
|
+
totalDuplicates += duplicates;
|
|
191
|
+
totalRejected += rejected;
|
|
192
|
+
const rejectedPart =
|
|
193
|
+
rejected > 0
|
|
194
|
+
? ` ${rejected} observation${rejected === 1 ? "" : "s"} rejected for missing or invalid sourceEntryIds.`
|
|
195
|
+
: "";
|
|
196
|
+
const ack =
|
|
197
|
+
`Recorded ${added} new observation${added === 1 ? "" : "s"} ` +
|
|
198
|
+
(duplicates > 0
|
|
199
|
+
? `(${duplicates} duplicate${duplicates === 1 ? "" : "s"} skipped).`
|
|
200
|
+
: ".") +
|
|
201
|
+
rejectedPart +
|
|
202
|
+
` Total so far this run: ${accumulated.size}. ` +
|
|
203
|
+
`Continue if the chunk still has uncovered content; otherwise stop calling the tool and emit a short plain-text confirmation.`;
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text", text: ack }],
|
|
206
|
+
details: { added, duplicates, rejected, total: accumulated.size },
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
};
|
|
180
210
|
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
const now = nowTimestamp();
|
|
212
|
+
const userText = `Current local time: ${now}
|
|
183
213
|
|
|
184
214
|
CURRENT REFLECTIONS:
|
|
185
215
|
${joinOrEmpty(priorReflections)}
|
|
@@ -192,83 +222,89 @@ Compress the following new conversation chunk into observations by calling recor
|
|
|
192
222
|
NEW CONVERSATION CHUNK:
|
|
193
223
|
${conversation}`;
|
|
194
224
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
const prompts: Message[] = [
|
|
226
|
+
{
|
|
227
|
+
role: "user",
|
|
228
|
+
content: [{ type: "text", text: userText }],
|
|
229
|
+
timestamp: Date.now(),
|
|
230
|
+
},
|
|
231
|
+
];
|
|
202
232
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
const context: AgentContext = {
|
|
234
|
+
systemPrompt: OBSERVER_SYSTEM,
|
|
235
|
+
messages: [],
|
|
236
|
+
tools: [recordObservations as AgentTool<any>],
|
|
237
|
+
};
|
|
208
238
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
const reasoning = (model as { reasoning?: unknown }).reasoning;
|
|
240
|
+
const thinkingLevel = args.thinkingLevel ?? "low";
|
|
241
|
+
const effectiveMaxTurns =
|
|
242
|
+
args.maxTurns && args.maxTurns > 0 ? args.maxTurns : undefined;
|
|
243
|
+
let turnCount = 0;
|
|
244
|
+
const config: AgentLoopConfig = {
|
|
245
|
+
model,
|
|
246
|
+
apiKey,
|
|
247
|
+
headers,
|
|
248
|
+
maxTokens: boundedMaxTokens(model, AGENT_LOOP_MAX_TOKENS),
|
|
249
|
+
convertToLlm: (msgs) => msgs as Message[],
|
|
250
|
+
toolExecution: "sequential",
|
|
251
|
+
...(reasoning && thinkingLevel !== "off"
|
|
252
|
+
? { reasoning: thinkingLevel }
|
|
253
|
+
: {}),
|
|
254
|
+
...(effectiveMaxTurns !== undefined
|
|
255
|
+
? {
|
|
256
|
+
shouldStopAfterTurn: () => {
|
|
257
|
+
turnCount++;
|
|
258
|
+
return turnCount >= effectiveMaxTurns;
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
: {}),
|
|
262
|
+
};
|
|
230
263
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
264
|
+
const loop = args.agentLoop ?? agentLoop;
|
|
265
|
+
// ── Bridge stream function ──
|
|
266
|
+
// Consolidation agents run via jiti (moduleCache: false) which creates a separate
|
|
267
|
+
// pi-ai instance whose apiProviderRegistry lacks custom providers registered by
|
|
268
|
+
// other extensions (e.g., claude-bridge). The bridge looks up streamSimple functions
|
|
269
|
+
const bridgeStreamFn = createBridgeStreamFn(streamSimple);
|
|
270
|
+
const streamFn = args.streamFn ?? bridgeStreamFn;
|
|
271
|
+
const stream = loop(prompts, context, config, signal, streamFn);
|
|
272
|
+
let agentError: string | undefined;
|
|
273
|
+
for await (const event of stream) {
|
|
274
|
+
// Drain events; the tool's execute already collects records.
|
|
275
|
+
if (event.type === "agent_end") {
|
|
276
|
+
const msgs = ((event as any).messages || []) as Array<{
|
|
277
|
+
stopReason?: string;
|
|
278
|
+
errorMessage?: string;
|
|
279
|
+
}>;
|
|
280
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
281
|
+
if (lastMsg?.stopReason === "error") {
|
|
282
|
+
agentError = lastMsg.errorMessage ?? "Unknown API error";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
await stream.result();
|
|
251
287
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
288
|
+
if (agentError && accumulated.size === 0) {
|
|
289
|
+
throw new Error(`Observer API error: ${agentError}`);
|
|
290
|
+
}
|
|
255
291
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
292
|
+
if (accumulated.size === 0) {
|
|
293
|
+
// Determine why no observations were recorded
|
|
294
|
+
let emptyReason: ObserverEmptyReason;
|
|
295
|
+
if (!toolCalled) {
|
|
296
|
+
emptyReason = { kind: "tool_not_called" };
|
|
297
|
+
} else if (totalRejected > 0 && totalAdded === 0) {
|
|
298
|
+
emptyReason = { kind: "all_rejected", count: totalRejected };
|
|
299
|
+
} else if (totalDuplicates > 0 && totalAdded === 0) {
|
|
300
|
+
emptyReason = { kind: "all_duplicates", count: totalDuplicates };
|
|
301
|
+
} else if (totalProposed === 0) {
|
|
302
|
+
emptyReason = { kind: "empty_array", count: 0 };
|
|
303
|
+
} else {
|
|
304
|
+
emptyReason = { kind: "no_new_content" };
|
|
305
|
+
}
|
|
306
|
+
return { observations: undefined, emptyReason };
|
|
307
|
+
}
|
|
272
308
|
|
|
273
|
-
|
|
309
|
+
return { observations: Array.from(accumulated.values()) };
|
|
274
310
|
}
|