pi-advisor-flow 0.6.0 → 0.8.0
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/CHANGELOG.md +34 -17
- package/README.md +10 -8
- package/dist/index.js +2852 -868
- package/extensions/index.ts +2 -1
- package/package.json +55 -40
- package/src/attachments.ts +11 -10
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +2 -0
- package/src/commands/lifecycle.ts +1 -0
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +103 -79
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +64 -2
- package/src/commands/types.ts +2 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +83 -1
- package/src/config/schema.ts +152 -15
- package/src/config/state.ts +80 -2
- package/src/config/storage.ts +38 -22
- package/src/config/types.ts +45 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +338 -0
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +267 -0
- package/src/jev/ledger.ts +228 -0
- package/src/jev/questions.ts +122 -0
- package/src/jev/state.ts +50 -0
- package/src/jev/transport.ts +63 -0
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +20 -12
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +115 -73
- package/src/session-state.ts +148 -39
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +204 -0
- package/src/tools/jev-turn-gate.ts +203 -0
- package/src/tools/loop-gate.ts +32 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/outage-notifier.ts +30 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +73 -17
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +149 -73
- package/src/tools/registration.ts +15 -0
- package/src/tools/render-advisor-result.ts +77 -35
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +11 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +350 -0
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +62 -0
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +200 -2
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +169 -47
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +25 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +29 -20
package/src/scout-groups.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
2
3
|
import type { AdvisorToolPolicies } from "./config/types.ts";
|
|
3
4
|
import {
|
|
4
5
|
byteLength,
|
|
5
6
|
contentParts,
|
|
6
7
|
isRecord,
|
|
7
|
-
|
|
8
|
+
isString,
|
|
8
9
|
} from "./content-utils.ts";
|
|
10
|
+
import type { RecordValue } from "./content-utils.ts";
|
|
9
11
|
import { conversationEntry } from "./conversation.ts";
|
|
10
12
|
import { invalid, toolCallId, toolCalls } from "./scout-protocol.ts";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import { SCOUT_LABEL_MAX_CHARS } from "./scout-types.ts";
|
|
14
|
+
import type {
|
|
15
|
+
InvalidProtocolResult,
|
|
16
|
+
ScoutContextGroup,
|
|
17
|
+
ScoutGroupKind,
|
|
16
18
|
} from "./scout-types.ts";
|
|
17
19
|
|
|
18
20
|
export interface GroupPass {
|
|
@@ -22,10 +24,10 @@ export interface GroupPass {
|
|
|
22
24
|
protocolOmittedCount: number;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const SPEAKER_PREFIX = /^(User|Executor):\s
|
|
27
|
+
const SPEAKER_PREFIX = /^(?<speaker>User|Executor):\s*/u;
|
|
26
28
|
|
|
27
29
|
const boundedLabel = (value: string) =>
|
|
28
|
-
[...value.
|
|
30
|
+
[...value.replaceAll(/\s+/gu, " ").trim()]
|
|
29
31
|
.slice(0, SCOUT_LABEL_MAX_CHARS)
|
|
30
32
|
.join("");
|
|
31
33
|
|
|
@@ -81,10 +83,8 @@ const createGroup = (
|
|
|
81
83
|
required,
|
|
82
84
|
});
|
|
83
85
|
|
|
84
|
-
const pendingAdvisorArguments = (
|
|
85
|
-
|
|
86
|
-
return {};
|
|
87
|
-
}
|
|
86
|
+
const pendingAdvisorArguments = (part: RecordValue): RecordValue => {
|
|
87
|
+
const value = isRecord(part.arguments) ? part.arguments : {};
|
|
88
88
|
const allowed: RecordValue = {};
|
|
89
89
|
for (const key of ["gitContext", "question"]) {
|
|
90
90
|
if (key in value) {
|
|
@@ -106,7 +106,8 @@ const pendingInvocationDisclosure = (
|
|
|
106
106
|
if (!isRecord(entry.message)) {
|
|
107
107
|
return disclosed;
|
|
108
108
|
}
|
|
109
|
-
const
|
|
109
|
+
const message: RecordValue = entry.message;
|
|
110
|
+
const content = contentParts(message.content).map((part) => {
|
|
110
111
|
if (
|
|
111
112
|
!isRecord(part) ||
|
|
112
113
|
part.type !== "toolCall" ||
|
|
@@ -115,10 +116,10 @@ const pendingInvocationDisclosure = (
|
|
|
115
116
|
) {
|
|
116
117
|
return part;
|
|
117
118
|
}
|
|
118
|
-
return { ...part, arguments: pendingAdvisorArguments(part
|
|
119
|
+
return { ...part, arguments: pendingAdvisorArguments(part) };
|
|
119
120
|
});
|
|
120
121
|
return conversationEntry(
|
|
121
|
-
{ ...entry, message: { ...
|
|
122
|
+
{ ...entry, message: { ...message, content } },
|
|
122
123
|
toolResultMaxLines,
|
|
123
124
|
toolResultMaxBytes,
|
|
124
125
|
policies,
|
|
@@ -139,88 +140,19 @@ type ToolExchangeOutcome =
|
|
|
139
140
|
| { kind: "omitted"; bytes: number }
|
|
140
141
|
| { kind: "invalid"; message: string };
|
|
141
142
|
|
|
142
|
-
/** Groups an assistant message with its tool calls and matched results into
|
|
143
|
-
* one indivisible tool-exchange (or pending-invocation) group. */
|
|
144
|
-
const toolExchangeGroup = (
|
|
145
|
-
entry: RecordValue,
|
|
146
|
-
index: number,
|
|
147
|
-
entryId: string,
|
|
148
|
-
disclosed: string,
|
|
149
|
-
immediate: unknown,
|
|
150
|
-
indexed: {
|
|
151
|
-
callOwners: Map<string, { index: number; name: string }>;
|
|
152
|
-
resultsByCall: Map<string, Array<{ entry: RecordValue; index: number }>>;
|
|
153
|
-
},
|
|
154
|
-
consumedResultIndexes: Set<number>,
|
|
155
|
-
caps: DisclosureCaps
|
|
156
|
-
): ToolExchangeOutcome => {
|
|
157
|
-
const calls = toolCalls(entry.message as RecordValue);
|
|
158
|
-
const callIds = calls.map(toolCallId) as string[];
|
|
159
|
-
const adjacentFailure = adjacentResultMismatch(
|
|
160
|
-
immediate,
|
|
161
|
-
index,
|
|
162
|
-
callIds,
|
|
163
|
-
indexed.callOwners
|
|
164
|
-
);
|
|
165
|
-
if (adjacentFailure) {
|
|
166
|
-
return { kind: "invalid", message: adjacentFailure };
|
|
167
|
-
}
|
|
168
|
-
const missing = new Set<string>();
|
|
169
|
-
const resultParts: string[] = [];
|
|
170
|
-
const resultEntryIds: string[] = [];
|
|
171
|
-
const matchFailure = collectResults(
|
|
172
|
-
calls,
|
|
173
|
-
callIds,
|
|
174
|
-
index,
|
|
175
|
-
indexed.resultsByCall,
|
|
176
|
-
consumedResultIndexes,
|
|
177
|
-
caps,
|
|
178
|
-
missing,
|
|
179
|
-
resultParts,
|
|
180
|
-
resultEntryIds
|
|
181
|
-
);
|
|
182
|
-
if (matchFailure) {
|
|
183
|
-
return { kind: "invalid", message: matchFailure };
|
|
184
|
-
}
|
|
185
|
-
if (missing.size > 0) {
|
|
186
|
-
return missingOutcome(
|
|
187
|
-
entry,
|
|
188
|
-
index,
|
|
189
|
-
entryId,
|
|
190
|
-
disclosed,
|
|
191
|
-
callIds,
|
|
192
|
-
missing,
|
|
193
|
-
resultParts,
|
|
194
|
-
resultEntryIds,
|
|
195
|
-
caps
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
return {
|
|
199
|
-
group: createGroup(
|
|
200
|
-
index,
|
|
201
|
-
[entryId, ...resultEntryIds],
|
|
202
|
-
"tool-exchange",
|
|
203
|
-
[disclosed, ...resultParts].join("\n\n"),
|
|
204
|
-
false
|
|
205
|
-
),
|
|
206
|
-
kind: "group",
|
|
207
|
-
};
|
|
208
|
-
};
|
|
209
|
-
|
|
210
143
|
const adjacentResultMismatch = (
|
|
211
|
-
immediate:
|
|
144
|
+
immediate: RecordValue | undefined,
|
|
212
145
|
index: number,
|
|
213
146
|
callIds: string[],
|
|
214
147
|
callOwners: Map<string, { index: number; name: string }>
|
|
215
148
|
): string | undefined => {
|
|
216
|
-
const next = immediate as RecordValue | undefined;
|
|
217
149
|
if (
|
|
218
|
-
|
|
219
|
-
isRecord(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
!callIds.includes(
|
|
223
|
-
!callOwners.has(
|
|
150
|
+
immediate?.type === "message" &&
|
|
151
|
+
isRecord(immediate.message) &&
|
|
152
|
+
immediate.message.role === "toolResult" &&
|
|
153
|
+
isString(immediate.message.toolCallId) &&
|
|
154
|
+
!callIds.includes(immediate.message.toolCallId) &&
|
|
155
|
+
!callOwners.has(immediate.message.toolCallId)
|
|
224
156
|
) {
|
|
225
157
|
return `Tool result at context entry ${index + 1} does not match its adjacent assistant group.`;
|
|
226
158
|
}
|
|
@@ -231,7 +163,7 @@ const collectResults = (
|
|
|
231
163
|
calls: RecordValue[],
|
|
232
164
|
callIds: string[],
|
|
233
165
|
index: number,
|
|
234
|
-
resultsByCall: Map<string,
|
|
166
|
+
resultsByCall: Map<string, { entry: RecordValue; index: number }[]>,
|
|
235
167
|
consumedResultIndexes: Set<number>,
|
|
236
168
|
caps: DisclosureCaps,
|
|
237
169
|
missing: Set<string>,
|
|
@@ -248,10 +180,8 @@ const collectResults = (
|
|
|
248
180
|
return `Tool result at context entry ${resultMatch.index} precedes its assistant call.`;
|
|
249
181
|
}
|
|
250
182
|
const resultMessage = resultMatch.entry.message;
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
? calls[callIndex].name
|
|
254
|
-
: "unknown";
|
|
183
|
+
const call = calls[callIndex];
|
|
184
|
+
const expectedName = isString(call.name) ? call.name : "unknown";
|
|
255
185
|
if (!isRecord(resultMessage) || resultMessage.toolName !== expectedName) {
|
|
256
186
|
return `Tool result at context entry ${resultMatch.index} conflicts with call ${callId}.`;
|
|
257
187
|
}
|
|
@@ -267,7 +197,7 @@ const collectResults = (
|
|
|
267
197
|
resultParts.push(resultText);
|
|
268
198
|
}
|
|
269
199
|
resultEntryIds.push(
|
|
270
|
-
|
|
200
|
+
isString(resultMatch.entry.id)
|
|
271
201
|
? resultMatch.entry.id
|
|
272
202
|
: String(resultMatch.index)
|
|
273
203
|
);
|
|
@@ -319,82 +249,130 @@ const missingOutcome = (
|
|
|
319
249
|
};
|
|
320
250
|
};
|
|
321
251
|
|
|
322
|
-
/**
|
|
323
|
-
*
|
|
324
|
-
|
|
325
|
-
|
|
252
|
+
/** Groups an assistant message with its tool calls and matched results into
|
|
253
|
+
* one indivisible tool-exchange (or pending-invocation) group. */
|
|
254
|
+
const toolExchangeGroup = (
|
|
255
|
+
entry: RecordValue,
|
|
256
|
+
message: RecordValue,
|
|
257
|
+
index: number,
|
|
258
|
+
entryId: string,
|
|
259
|
+
disclosed: string,
|
|
260
|
+
immediate: RecordValue | undefined,
|
|
326
261
|
indexed: {
|
|
327
262
|
callOwners: Map<string, { index: number; name: string }>;
|
|
328
|
-
|
|
329
|
-
resultsByCall: Map<string, Array<{ entry: RecordValue; index: number }>>;
|
|
263
|
+
resultsByCall: Map<string, { entry: RecordValue; index: number }[]>;
|
|
330
264
|
},
|
|
331
|
-
|
|
332
|
-
|
|
265
|
+
consumedResultIndexes: Set<number>,
|
|
266
|
+
caps: DisclosureCaps
|
|
267
|
+
): ToolExchangeOutcome => {
|
|
268
|
+
const calls = toolCalls(message);
|
|
269
|
+
const callIds = calls.map(toolCallId).filter(isString);
|
|
270
|
+
const adjacentFailure = adjacentResultMismatch(
|
|
271
|
+
immediate,
|
|
272
|
+
index,
|
|
273
|
+
callIds,
|
|
274
|
+
indexed.callOwners
|
|
275
|
+
);
|
|
276
|
+
if (adjacentFailure) {
|
|
277
|
+
return { kind: "invalid", message: adjacentFailure };
|
|
333
278
|
}
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
279
|
+
const missing = new Set<string>();
|
|
280
|
+
const resultParts: string[] = [];
|
|
281
|
+
const resultEntryIds: string[] = [];
|
|
282
|
+
const matchFailure = collectResults(
|
|
283
|
+
calls,
|
|
284
|
+
callIds,
|
|
285
|
+
index,
|
|
286
|
+
indexed.resultsByCall,
|
|
287
|
+
consumedResultIndexes,
|
|
288
|
+
caps,
|
|
289
|
+
missing,
|
|
290
|
+
resultParts,
|
|
291
|
+
resultEntryIds
|
|
292
|
+
);
|
|
293
|
+
if (matchFailure) {
|
|
294
|
+
return { kind: "invalid", message: matchFailure };
|
|
295
|
+
}
|
|
296
|
+
if (missing.size > 0) {
|
|
297
|
+
return missingOutcome(
|
|
352
298
|
entry,
|
|
353
299
|
index,
|
|
300
|
+
entryId,
|
|
354
301
|
disclosed,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
302
|
+
callIds,
|
|
303
|
+
missing,
|
|
304
|
+
resultParts,
|
|
305
|
+
resultEntryIds,
|
|
358
306
|
caps
|
|
359
307
|
);
|
|
360
|
-
if (outcome.kind === "invalid") {
|
|
361
|
-
return invalid(outcome.message);
|
|
362
|
-
}
|
|
363
|
-
if (outcome.kind === "omitted") {
|
|
364
|
-
protocolOmittedCount += 1;
|
|
365
|
-
protocolOmittedBytes += outcome.bytes;
|
|
366
|
-
} else if (outcome.kind === "group") {
|
|
367
|
-
groups.push(outcome.group);
|
|
368
|
-
}
|
|
369
308
|
}
|
|
370
309
|
return {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
310
|
+
group: createGroup(
|
|
311
|
+
index,
|
|
312
|
+
[entryId, ...resultEntryIds],
|
|
313
|
+
"tool-exchange",
|
|
314
|
+
[disclosed, ...resultParts].join("\n\n"),
|
|
315
|
+
false
|
|
316
|
+
),
|
|
317
|
+
kind: "group",
|
|
375
318
|
};
|
|
376
319
|
};
|
|
377
320
|
|
|
321
|
+
const ownerOf = (
|
|
322
|
+
message: RecordValue,
|
|
323
|
+
callOwners: Map<string, { index: number; name: string }>
|
|
324
|
+
) =>
|
|
325
|
+
isString(message.toolCallId) ? callOwners.get(message.toolCallId) : undefined;
|
|
326
|
+
|
|
327
|
+
const contentPartsOf = (message: RecordValue) =>
|
|
328
|
+
Array.isArray(message.content) ? message.content : [];
|
|
329
|
+
|
|
330
|
+
const hasCalls = (message: RecordValue) =>
|
|
331
|
+
contentPartsOf(message).some(
|
|
332
|
+
(part) => isRecord(part) && part.type === "toolCall"
|
|
333
|
+
);
|
|
334
|
+
|
|
378
335
|
type EntryOutcome =
|
|
379
336
|
| { kind: "group"; group: ScoutContextGroup }
|
|
380
337
|
| { kind: "omitted"; bytes: number }
|
|
381
338
|
| { kind: "invalid"; message: string }
|
|
382
339
|
| { kind: "skipped" };
|
|
383
340
|
|
|
341
|
+
const toolResultOutcome = (
|
|
342
|
+
message: RecordValue,
|
|
343
|
+
index: number,
|
|
344
|
+
disclosed: string,
|
|
345
|
+
callOwners: Map<string, { index: number; name: string }>,
|
|
346
|
+
consumedResultIndexes: Set<number>
|
|
347
|
+
): EntryOutcome => {
|
|
348
|
+
if (consumedResultIndexes.has(index)) {
|
|
349
|
+
return { kind: "skipped" };
|
|
350
|
+
}
|
|
351
|
+
if (ownerOf(message, callOwners)) {
|
|
352
|
+
return {
|
|
353
|
+
kind: "invalid",
|
|
354
|
+
message: `Tool result at context entry ${index} precedes or conflicts with its retained call.`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// Retained results without their call are unavailable optional evidence
|
|
358
|
+
// and are never offered to Scout.
|
|
359
|
+
return { bytes: byteLength(disclosed), kind: "omitted" };
|
|
360
|
+
};
|
|
361
|
+
|
|
384
362
|
const groupForEntry = (
|
|
385
363
|
entry: RecordValue,
|
|
386
364
|
index: number,
|
|
387
365
|
disclosed: string,
|
|
388
|
-
immediate:
|
|
366
|
+
immediate: RecordValue | undefined,
|
|
389
367
|
indexed: {
|
|
390
368
|
callOwners: Map<string, { index: number; name: string }>;
|
|
391
369
|
latestUserIndex: number;
|
|
392
|
-
resultsByCall: Map<string,
|
|
370
|
+
resultsByCall: Map<string, { entry: RecordValue; index: number }[]>;
|
|
393
371
|
},
|
|
394
372
|
consumedResultIndexes: Set<number>,
|
|
395
373
|
caps: DisclosureCaps
|
|
396
374
|
): EntryOutcome => {
|
|
397
|
-
const entryId =
|
|
375
|
+
const entryId = isString(entry.id) ? entry.id : String(index);
|
|
398
376
|
if (entry.type !== "message" || !isRecord(entry.message)) {
|
|
399
377
|
return {
|
|
400
378
|
group: createGroup(index, [entryId], "compaction", disclosed, false),
|
|
@@ -434,6 +412,7 @@ const groupForEntry = (
|
|
|
434
412
|
}
|
|
435
413
|
return toolExchangeGroup(
|
|
436
414
|
entry,
|
|
415
|
+
message,
|
|
437
416
|
index,
|
|
438
417
|
entryId,
|
|
439
418
|
disclosed,
|
|
@@ -444,42 +423,66 @@ const groupForEntry = (
|
|
|
444
423
|
);
|
|
445
424
|
};
|
|
446
425
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
426
|
+
/** Second entry pass: groups each disclosed entry, delegating
|
|
427
|
+
* assistant-with-calls entries to the tool-exchange builder. */
|
|
428
|
+
export const buildGroups = (
|
|
429
|
+
entries: unknown[],
|
|
430
|
+
indexed: {
|
|
431
|
+
callOwners: Map<string, { index: number; name: string }>;
|
|
432
|
+
latestUserIndex: number;
|
|
433
|
+
resultsByCall: Map<string, { entry: RecordValue; index: number }[]>;
|
|
434
|
+
},
|
|
435
|
+
caps: DisclosureCaps & {
|
|
436
|
+
maxGroupBytes: number;
|
|
456
437
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
438
|
+
): GroupPass | InvalidProtocolResult => {
|
|
439
|
+
const groups: ScoutContextGroup[] = [];
|
|
440
|
+
const consumedResultIndexes = new Set<number>();
|
|
441
|
+
let protocolOmittedBytes = 0;
|
|
442
|
+
let protocolOmittedCount = 0;
|
|
443
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
444
|
+
const entry = entries[index];
|
|
445
|
+
if (!isRecord(entry)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const disclosed = conversationEntry(
|
|
449
|
+
entry,
|
|
450
|
+
caps.toolResultMaxLines,
|
|
451
|
+
caps.toolResultMaxBytes,
|
|
452
|
+
caps.policies,
|
|
453
|
+
caps.redact
|
|
454
|
+
);
|
|
455
|
+
if (!disclosed) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const next = entries[index + 1];
|
|
459
|
+
const immediate = isRecord(next) ? next : undefined;
|
|
460
|
+
const outcome = groupForEntry(
|
|
461
|
+
entry,
|
|
462
|
+
index,
|
|
463
|
+
disclosed,
|
|
464
|
+
immediate,
|
|
465
|
+
indexed,
|
|
466
|
+
consumedResultIndexes,
|
|
467
|
+
caps
|
|
468
|
+
);
|
|
469
|
+
if (outcome.kind === "invalid") {
|
|
470
|
+
return invalid(outcome.message);
|
|
471
|
+
}
|
|
472
|
+
if (outcome.kind === "omitted") {
|
|
473
|
+
protocolOmittedCount += 1;
|
|
474
|
+
protocolOmittedBytes += outcome.bytes;
|
|
475
|
+
} else if (outcome.kind === "group") {
|
|
476
|
+
groups.push(outcome.group);
|
|
477
|
+
}
|
|
462
478
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
479
|
+
return {
|
|
480
|
+
groups,
|
|
481
|
+
ok: true,
|
|
482
|
+
protocolOmittedBytes,
|
|
483
|
+
protocolOmittedCount,
|
|
484
|
+
};
|
|
466
485
|
};
|
|
467
486
|
|
|
468
|
-
const ownerOf = (
|
|
469
|
-
message: RecordValue,
|
|
470
|
-
callOwners: Map<string, { index: number; name: string }>
|
|
471
|
-
) =>
|
|
472
|
-
typeof message.toolCallId === "string"
|
|
473
|
-
? callOwners.get(message.toolCallId)
|
|
474
|
-
: undefined;
|
|
475
|
-
|
|
476
|
-
const hasCalls = (message: RecordValue) =>
|
|
477
|
-
contentPartsOf(message).some(
|
|
478
|
-
(part) => isRecord(part) && part.type === "toolCall"
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
const contentPartsOf = (message: RecordValue) =>
|
|
482
|
-
Array.isArray(message.content) ? message.content : [];
|
|
483
|
-
|
|
484
487
|
/** Final pass: selects groups within the transport and conversation budgets,
|
|
485
488
|
* keeping every required group. */
|
package/src/scout-protocol.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { contentParts, isRecord,
|
|
1
|
+
import { contentParts, isRecord, isString } from "./content-utils.ts";
|
|
2
|
+
import type { RecordValue } from "./content-utils.ts";
|
|
2
3
|
import { textFrom } from "./conversation.ts";
|
|
3
4
|
import type { InvalidProtocolResult } from "./scout-types.ts";
|
|
4
5
|
|
|
@@ -12,7 +13,7 @@ export const invalid = (message: string): InvalidProtocolResult => ({
|
|
|
12
13
|
export interface ToolCallIndex {
|
|
13
14
|
callOwners: Map<string, { index: number; name: string }>;
|
|
14
15
|
latestUserIndex: number;
|
|
15
|
-
resultsByCall: Map<string,
|
|
16
|
+
resultsByCall: Map<string, { entry: RecordValue; index: number }[]>;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const toolCalls = (message: RecordValue) =>
|
|
@@ -21,51 +22,51 @@ export const toolCalls = (message: RecordValue) =>
|
|
|
21
22
|
);
|
|
22
23
|
|
|
23
24
|
export const toolCallId = (part: RecordValue) =>
|
|
24
|
-
|
|
25
|
+
isString(part.id) ? part.id : undefined;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
):
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
38
|
-
const failure = indexEntry(
|
|
39
|
-
entries[index] as unknown as RecordValue,
|
|
40
|
-
index,
|
|
41
|
-
callOwners,
|
|
42
|
-
resultsByCall,
|
|
43
|
-
state
|
|
44
|
-
);
|
|
45
|
-
if (failure) {
|
|
46
|
-
return failure;
|
|
27
|
+
const indexAssistantCalls = (
|
|
28
|
+
message: RecordValue,
|
|
29
|
+
index: number,
|
|
30
|
+
callOwners: Map<string, { index: number; name: string }>
|
|
31
|
+
): InvalidProtocolResult | undefined => {
|
|
32
|
+
for (const call of toolCalls(message)) {
|
|
33
|
+
const id = toolCallId(call);
|
|
34
|
+
if (!id || callOwners.has(id)) {
|
|
35
|
+
return invalid(
|
|
36
|
+
`Assistant tool calls at context entry ${index} have missing or duplicate IDs.`
|
|
37
|
+
);
|
|
47
38
|
}
|
|
39
|
+
callOwners.set(id, {
|
|
40
|
+
index,
|
|
41
|
+
name: isString(call.name) ? call.name : "unknown",
|
|
42
|
+
});
|
|
48
43
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const indexToolResult = (
|
|
48
|
+
entry: RecordValue,
|
|
49
|
+
message: RecordValue,
|
|
50
|
+
index: number,
|
|
51
|
+
resultsByCall: Map<string, { entry: RecordValue; index: number }[]>
|
|
52
|
+
): InvalidProtocolResult | undefined => {
|
|
53
|
+
const id = message.toolCallId;
|
|
54
|
+
if (!isString(id)) {
|
|
55
|
+
return invalid(
|
|
56
|
+
`Tool result at context entry ${index} has no tool-call ID.`
|
|
57
|
+
);
|
|
53
58
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
resultsByCall,
|
|
59
|
-
},
|
|
60
|
-
ok: true,
|
|
61
|
-
};
|
|
59
|
+
const results = resultsByCall.get(id) ?? [];
|
|
60
|
+
results.push({ entry, index });
|
|
61
|
+
resultsByCall.set(id, results);
|
|
62
|
+
return undefined;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
const indexEntry = (
|
|
65
66
|
entry: RecordValue,
|
|
66
67
|
index: number,
|
|
67
68
|
callOwners: Map<string, { index: number; name: string }>,
|
|
68
|
-
resultsByCall: Map<string,
|
|
69
|
+
resultsByCall: Map<string, { entry: RecordValue; index: number }[]>,
|
|
69
70
|
state: { latestUserIndex: number }
|
|
70
71
|
): InvalidProtocolResult | undefined => {
|
|
71
72
|
if (entry.type !== "message" || !isRecord(entry.message)) {
|
|
@@ -83,40 +84,43 @@ const indexEntry = (
|
|
|
83
84
|
return undefined;
|
|
84
85
|
};
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
index: number,
|
|
107
|
-
callOwners: Map<string, { index: number; name: string }>
|
|
108
|
-
): InvalidProtocolResult | undefined => {
|
|
109
|
-
for (const call of toolCalls(message)) {
|
|
110
|
-
const id = toolCallId(call);
|
|
111
|
-
if (!id || callOwners.has(id)) {
|
|
112
|
-
return invalid(
|
|
113
|
-
`Assistant tool calls at context entry ${index} have missing or duplicate IDs.`
|
|
87
|
+
/** First entry pass: indexes assistant tool calls and their results, tracking
|
|
88
|
+
* the latest user message. Returns an invalid result on protocol violations. */
|
|
89
|
+
export const indexToolCalls = (
|
|
90
|
+
entries: unknown[]
|
|
91
|
+
): { index: ToolCallIndex; ok: true } | InvalidProtocolResult => {
|
|
92
|
+
const callOwners = new Map<string, { index: number; name: string }>();
|
|
93
|
+
const resultsByCall = new Map<
|
|
94
|
+
string,
|
|
95
|
+
{ entry: RecordValue; index: number }[]
|
|
96
|
+
>();
|
|
97
|
+
const state = { latestUserIndex: -1 };
|
|
98
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
99
|
+
const entry = entries[index];
|
|
100
|
+
if (isRecord(entry)) {
|
|
101
|
+
const failure = indexEntry(
|
|
102
|
+
entry,
|
|
103
|
+
index,
|
|
104
|
+
callOwners,
|
|
105
|
+
resultsByCall,
|
|
106
|
+
state
|
|
114
107
|
);
|
|
108
|
+
if (failure) {
|
|
109
|
+
return failure;
|
|
110
|
+
}
|
|
115
111
|
}
|
|
116
|
-
callOwners.set(id, {
|
|
117
|
-
index,
|
|
118
|
-
name: typeof call.name === "string" ? call.name : "unknown",
|
|
119
|
-
});
|
|
120
112
|
}
|
|
121
|
-
|
|
113
|
+
for (const [id, results] of resultsByCall) {
|
|
114
|
+
if (results.length > 1) {
|
|
115
|
+
return invalid(`Tool call ${id} has duplicate result messages.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
index: {
|
|
120
|
+
callOwners,
|
|
121
|
+
latestUserIndex: state.latestUserIndex,
|
|
122
|
+
resultsByCall,
|
|
123
|
+
},
|
|
124
|
+
ok: true,
|
|
125
|
+
};
|
|
122
126
|
};
|
package/src/scout-reconstruct.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const reconstructScoutConversation = (
|
|
|
20
20
|
const selected = new Set(selectedIds);
|
|
21
21
|
const evidence = manifest.groups
|
|
22
22
|
.filter((group) => group.required || selected.has(group.id))
|
|
23
|
-
.
|
|
23
|
+
.toSorted((left, right) => left.originalIndex - right.originalIndex)
|
|
24
24
|
.map((group) => group.content);
|
|
25
25
|
const evidenceText = evidence.join("\n\n");
|
|
26
26
|
if (maxChars <= 0) {
|