pi-advisor-flow 0.7.0 → 0.8.1-dev.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.
- package/CHANGELOG.md +27 -18
- package/README.md +9 -8
- package/dist/index.js +1968 -1081
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/child-session.ts +15 -0
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +12 -3
- package/src/commands/lifecycle.ts +7 -1
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +102 -92
- 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 +16 -2
- package/src/commands/types.ts +1 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/schema.ts +42 -17
- package/src/config/state.ts +12 -3
- package/src/config/storage.ts +34 -23
- package/src/config/types.ts +16 -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 +75 -85
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +26 -11
- package/src/jev/ledger.ts +58 -64
- package/src/jev/questions.ts +26 -23
- package/src/jev/state.ts +15 -12
- package/src/jev/transport.ts +8 -3
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +15 -11
- 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 +128 -77
- package/src/session-state.ts +64 -49
- 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 +43 -41
- package/src/tools/jev-turn-gate.ts +29 -9
- package/src/tools/loop-gate.ts +31 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +36 -16
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +146 -128
- package/src/tools/registration.ts +12 -14
- package/src/tools/render-advisor-result.ts +35 -22
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +1 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +65 -59
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +5 -11
- 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 +38 -1
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +157 -70
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +14 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +26 -19
package/src/scout-context.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import {
|
|
3
4
|
advisorRedactSecretsRef,
|
|
4
5
|
advisorToolPoliciesRef,
|
|
5
6
|
advisorToolResultMaxBytesRef,
|
|
6
7
|
advisorToolResultMaxLinesRef,
|
|
7
8
|
} from "./config/state.ts";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
type DisclosureCaps,
|
|
11
|
-
type GroupPass,
|
|
12
|
-
groupWireBytes,
|
|
13
|
-
} from "./scout-groups.ts";
|
|
9
|
+
import { buildGroups, groupWireBytes } from "./scout-groups.ts";
|
|
10
|
+
import type { DisclosureCaps, GroupPass } from "./scout-groups.ts";
|
|
14
11
|
import { indexToolCalls } from "./scout-protocol.ts";
|
|
15
12
|
import {
|
|
16
|
-
type BuildScoutManifestOptions,
|
|
17
13
|
SCOUT_GROUP_MAX_BYTES,
|
|
18
14
|
SCOUT_MANIFEST_MAX_BYTES,
|
|
19
15
|
SCOUT_MANIFEST_MAX_GROUPS,
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
} from "./scout-types.ts";
|
|
17
|
+
import type {
|
|
18
|
+
BuildScoutManifestOptions,
|
|
19
|
+
ScoutContextGroup,
|
|
20
|
+
ScoutManifestResult,
|
|
22
21
|
} from "./scout-types.ts";
|
|
23
22
|
|
|
24
|
-
//
|
|
23
|
+
// Re-exports preserve scout-context's historical public import surface.
|
|
25
24
|
export { reconstructScoutConversation } from "./scout-reconstruct.ts";
|
|
26
25
|
export type {
|
|
27
26
|
BuildScoutManifestOptions,
|
|
@@ -54,21 +53,44 @@ const resolveCaps = (
|
|
|
54
53
|
});
|
|
55
54
|
|
|
56
55
|
/** Builds disclosed, indivisible history groups from Pi's compaction-aware branch. */
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
const contentChars = (items: ScoutContextGroup[]) =>
|
|
57
|
+
items.reduce((sum, group) => sum + group.content.length, 0) +
|
|
58
|
+
Math.max(0, items.length - 1) * 2;
|
|
59
|
+
|
|
60
|
+
const requiredOverflow = (
|
|
61
|
+
required: ScoutContextGroup[],
|
|
62
|
+
caps: {
|
|
63
|
+
maxConversationChars?: number;
|
|
64
|
+
maxGroupBytes: number;
|
|
65
|
+
maxGroups: number;
|
|
66
|
+
maxManifestBytes: number;
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
68
|
+
): ScoutManifestResult | undefined => {
|
|
69
|
+
if (
|
|
70
|
+
required.some((group) => group.bytes > caps.maxGroupBytes) ||
|
|
71
|
+
required.length > caps.maxGroups ||
|
|
72
|
+
required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
|
|
73
|
+
caps.maxManifestBytes
|
|
74
|
+
) {
|
|
75
|
+
return {
|
|
76
|
+
message:
|
|
77
|
+
"Required Scout context exceeds the Scout manifest transport limit.",
|
|
78
|
+
ok: false,
|
|
79
|
+
reason: "required-group-overflow",
|
|
80
|
+
};
|
|
70
81
|
}
|
|
71
|
-
|
|
82
|
+
if (
|
|
83
|
+
caps.maxConversationChars !== undefined &&
|
|
84
|
+
contentChars(required) > caps.maxConversationChars
|
|
85
|
+
) {
|
|
86
|
+
return {
|
|
87
|
+
message:
|
|
88
|
+
"Required Scout context exceeds the Advisor conversation budget.",
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: "required-group-overflow",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
72
94
|
};
|
|
73
95
|
|
|
74
96
|
const fits = (
|
|
@@ -86,10 +108,6 @@ const fits = (
|
|
|
86
108
|
(caps.maxConversationChars === undefined ||
|
|
87
109
|
contentChars(selected) <= caps.maxConversationChars);
|
|
88
110
|
|
|
89
|
-
const contentChars = (items: ScoutContextGroup[]) =>
|
|
90
|
-
items.reduce((sum, group) => sum + group.content.length, 0) +
|
|
91
|
-
Math.max(0, items.length - 1) * 2;
|
|
92
|
-
|
|
93
111
|
const fitToBudget = (
|
|
94
112
|
built: GroupPass,
|
|
95
113
|
caps: {
|
|
@@ -126,7 +144,7 @@ const fitToBudget = (
|
|
|
126
144
|
);
|
|
127
145
|
while (!fits(selected, caps)) {
|
|
128
146
|
const optionalIndex = selected.findIndex((group) => !group.required);
|
|
129
|
-
if (optionalIndex
|
|
147
|
+
if (optionalIndex === -1) {
|
|
130
148
|
return {
|
|
131
149
|
message: "Required Scout context exceeds fixed manifest limits.",
|
|
132
150
|
ok: false,
|
|
@@ -151,38 +169,19 @@ const fitToBudget = (
|
|
|
151
169
|
};
|
|
152
170
|
};
|
|
153
171
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
required.some((group) => group.bytes > caps.maxGroupBytes) ||
|
|
165
|
-
required.length > caps.maxGroups ||
|
|
166
|
-
required.reduce((sum, group) => sum + groupWireBytes(group), 0) >
|
|
167
|
-
caps.maxManifestBytes
|
|
168
|
-
) {
|
|
169
|
-
return {
|
|
170
|
-
message:
|
|
171
|
-
"Required Scout context exceeds the Scout manifest transport limit.",
|
|
172
|
-
ok: false,
|
|
173
|
-
reason: "required-group-overflow",
|
|
174
|
-
};
|
|
172
|
+
export const buildScoutManifest = (
|
|
173
|
+
ctx: ExtensionContext,
|
|
174
|
+
options: BuildScoutManifestOptions = {}
|
|
175
|
+
): ScoutManifestResult => {
|
|
176
|
+
const entries = ctx.sessionManager.buildContextEntries();
|
|
177
|
+
const caps = resolveCaps(options);
|
|
178
|
+
const indexed = indexToolCalls(entries);
|
|
179
|
+
if (!indexed.ok) {
|
|
180
|
+
return indexed;
|
|
175
181
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
) {
|
|
180
|
-
return {
|
|
181
|
-
message:
|
|
182
|
-
"Required Scout context exceeds the Advisor conversation budget.",
|
|
183
|
-
ok: false,
|
|
184
|
-
reason: "required-group-overflow",
|
|
185
|
-
};
|
|
182
|
+
const built = buildGroups(entries, indexed.index, caps);
|
|
183
|
+
if (!built.ok) {
|
|
184
|
+
return built;
|
|
186
185
|
}
|
|
187
|
-
return
|
|
186
|
+
return fitToBudget(built, caps);
|
|
188
187
|
};
|
package/src/scout-curation.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
2
3
|
import { advisorScoutEnabledRef, executorRef } from "./config/state.ts";
|
|
3
|
-
import {
|
|
4
|
-
runAdvisorScout,
|
|
5
|
-
type ScoutLifecycleEvent,
|
|
6
|
-
type ScoutOutcome,
|
|
7
|
-
} from "./scout.ts";
|
|
8
4
|
import {
|
|
9
5
|
buildScoutManifest,
|
|
10
6
|
reconstructScoutConversation,
|
|
11
7
|
SCOUT_MANIFEST_MAX_BYTES,
|
|
12
8
|
} from "./scout-context.ts";
|
|
9
|
+
import { runAdvisorScout } from "./scout.ts";
|
|
10
|
+
import type { ScoutLifecycleEvent, ScoutOutcome } from "./scout.ts";
|
|
13
11
|
|
|
14
12
|
/** Runs Scout over the manifested branch and reconstructs the curated
|
|
15
13
|
* conversation, falling back to the exact legacy conversation on any
|
|
@@ -55,14 +53,7 @@ export const curateAdvisorConversation = async (
|
|
|
55
53
|
onScout?.({ outcome: scout, type: "fallback" });
|
|
56
54
|
return { conversation: legacyConversation, scout };
|
|
57
55
|
}
|
|
58
|
-
const outcome = await runScout(
|
|
59
|
-
ctx,
|
|
60
|
-
built.manifest,
|
|
61
|
-
signal,
|
|
62
|
-
onScout,
|
|
63
|
-
undefined,
|
|
64
|
-
undefined
|
|
65
|
-
);
|
|
56
|
+
const outcome = await runScout(ctx, built.manifest, signal, onScout);
|
|
66
57
|
if (!outcome.ok && outcome.cancelled) {
|
|
67
58
|
throw signal?.reason instanceof Error
|
|
68
59
|
? signal.reason
|
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. */
|