pi-advisor-flow 0.1.9 → 0.2.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/README.md +15 -11
- package/extensions/index.ts +17 -1
- package/package.json +25 -21
- package/src/commands.ts +346 -99
- package/src/config.ts +345 -94
- package/src/conversation.ts +199 -48
- package/src/herdr.ts +145 -30
- package/src/session-state.ts +182 -41
- package/src/tools.ts +687 -179
- package/src/ui.ts +317 -81
package/src/tools.ts
CHANGED
|
@@ -1,46 +1,130 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type AssistantMessage,
|
|
3
|
+
type Message,
|
|
4
|
+
stream,
|
|
5
|
+
} from "@earendil-works/pi-ai/compat";
|
|
6
|
+
import {
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
getMarkdownTheme,
|
|
10
|
+
type Theme,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3
12
|
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
4
13
|
import { Type } from "typebox";
|
|
5
14
|
import {
|
|
6
|
-
advisorAutoLoopGateRef,
|
|
7
|
-
|
|
15
|
+
advisorAutoLoopGateRef,
|
|
16
|
+
advisorBlockOnBlockedRef,
|
|
17
|
+
advisorCollapseResponsesRef,
|
|
18
|
+
advisorCompletionGateRef,
|
|
19
|
+
advisorCustomInvocationRef,
|
|
20
|
+
advisorEffortRef,
|
|
21
|
+
advisorFailureGateRef,
|
|
22
|
+
advisorFailureModeRef,
|
|
23
|
+
advisorLoopThresholdRef,
|
|
24
|
+
advisorMaxCallsPerSessionRef,
|
|
25
|
+
advisorPlanGateRef,
|
|
26
|
+
advisorRef,
|
|
27
|
+
advisorSessionSummaryRef,
|
|
28
|
+
contextMaxCharsRef,
|
|
29
|
+
loadConfig,
|
|
30
|
+
splitRef,
|
|
8
31
|
} from "./config.js";
|
|
9
32
|
import { recentConversation, textFrom } from "./conversation.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
33
|
+
import {
|
|
34
|
+
herdrAdvisorActivity,
|
|
35
|
+
herdrAdvisorBlock,
|
|
36
|
+
notifyHerdrAdvisorFailure,
|
|
37
|
+
} from "./herdr.js";
|
|
38
|
+
import {
|
|
39
|
+
AdvisorSessionState,
|
|
40
|
+
type ConsultationTrigger,
|
|
41
|
+
type GateDecision,
|
|
42
|
+
type GateTrigger,
|
|
43
|
+
} from "./session-state.js";
|
|
44
|
+
|
|
45
|
+
export type {
|
|
46
|
+
AdvisorInvocationRecord,
|
|
47
|
+
ConsultationTrigger,
|
|
48
|
+
GateDecision,
|
|
49
|
+
GateTrigger,
|
|
50
|
+
} from "./session-state.js";
|
|
12
51
|
|
|
13
52
|
export const advisorSessionState = new AdvisorSessionState();
|
|
14
53
|
|
|
15
|
-
export const SPINNER_FRAMES = [
|
|
16
|
-
|
|
54
|
+
export const SPINNER_FRAMES = [
|
|
55
|
+
"⠋",
|
|
56
|
+
"⠙",
|
|
57
|
+
"⠹",
|
|
58
|
+
"⠸",
|
|
59
|
+
"⠼",
|
|
60
|
+
"⠴",
|
|
61
|
+
"⠦",
|
|
62
|
+
"⠧",
|
|
63
|
+
"⠇",
|
|
64
|
+
"⠏",
|
|
65
|
+
];
|
|
66
|
+
export const resolveAdvisorRequest = (question?: string) =>
|
|
67
|
+
question?.trim() || undefined;
|
|
17
68
|
export const advisorMessageText = (conversation: string, question?: string) =>
|
|
18
69
|
`${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
|
|
19
70
|
|
|
20
|
-
export const renderAdvisorCallBox = (
|
|
71
|
+
export const renderAdvisorCallBox = (
|
|
72
|
+
question: string | undefined,
|
|
73
|
+
theme: Theme
|
|
74
|
+
) => {
|
|
21
75
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
22
76
|
const label = theme.fg("customMessageLabel", theme.bold("[advisor]"));
|
|
23
77
|
const title = theme.fg("customMessageText", "Executor → Advisor");
|
|
24
|
-
box.addChild(
|
|
78
|
+
box.addChild(
|
|
79
|
+
new Text(
|
|
80
|
+
question
|
|
81
|
+
? `${label} ${title}\n${theme.fg("dim", ` ${question}`)}`
|
|
82
|
+
: `${label} ${title}`,
|
|
83
|
+
0,
|
|
84
|
+
0
|
|
85
|
+
)
|
|
86
|
+
);
|
|
25
87
|
return box;
|
|
26
88
|
};
|
|
27
89
|
|
|
28
90
|
const COLLAPSED_ADVICE_LINES = 12;
|
|
29
91
|
|
|
30
92
|
export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
31
|
-
if (!advisorCollapseResponsesRef || expanded)
|
|
93
|
+
if (!advisorCollapseResponsesRef || expanded) {
|
|
94
|
+
return advice;
|
|
95
|
+
}
|
|
32
96
|
const lines = advice.split("\n");
|
|
33
|
-
if (lines.length <= COLLAPSED_ADVICE_LINES)
|
|
97
|
+
if (lines.length <= COLLAPSED_ADVICE_LINES) {
|
|
98
|
+
return advice;
|
|
99
|
+
}
|
|
34
100
|
return `${lines.slice(0, COLLAPSED_ADVICE_LINES).join("\n")}\n\n… (${lines.length - COLLAPSED_ADVICE_LINES} more lines, Ctrl+O to expand)`;
|
|
35
101
|
};
|
|
36
102
|
|
|
37
103
|
export const advisorInvocationGuidelines = () => {
|
|
38
104
|
const guidelines: string[] = [];
|
|
39
|
-
if (advisorPlanGateRef)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
105
|
+
if (advisorPlanGateRef) {
|
|
106
|
+
guidelines.push(
|
|
107
|
+
"Before committing to a materially consequential plan, use ask_advisor after investigating and forming your own candidate direction. Use it to stress-test consequential architectural, security, data-loss, compatibility, or difficult-to-reverse decisions. Do not delegate the entire plan or task."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (advisorFailureGateRef) {
|
|
111
|
+
guidelines.push(
|
|
112
|
+
"Use ask_advisor after two consecutive materially equivalent failed attempts, when a fix recreates an earlier failure, or after two actions produce no measurable progress. Do not make another materially equivalent attempt before consulting."
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (advisorCompletionGateRef) {
|
|
116
|
+
guidelines.push(
|
|
117
|
+
"Before declaring success, use ask_advisor to review the goal, changed files, key decisions, tests, results, and remaining risks. Skip this only for demonstrably trivial, low-risk work."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (advisorCustomInvocationRef) {
|
|
121
|
+
guidelines.push(`Also use ask_advisor when: ${advisorCustomInvocationRef}`);
|
|
122
|
+
}
|
|
123
|
+
if (guidelines.length > 0) {
|
|
124
|
+
guidelines.push(
|
|
125
|
+
"Call ask_advisor with an empty object by default. Do not invent a question merely to request a review: the Advisor already receives context. Include question only for a genuinely specific assumption or trade-off."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
44
128
|
return guidelines;
|
|
45
129
|
};
|
|
46
130
|
|
|
@@ -55,70 +139,159 @@ export const ADVISOR_SYSTEM = [
|
|
|
55
139
|
export const ADVISOR_DECISION_SYSTEM = [
|
|
56
140
|
"You are the Advisor's automatic safety gate for a repeated-tool loop.",
|
|
57
141
|
"Review the supplied context and decide whether the Executor may proceed.",
|
|
58
|
-
"Answer in concise Markdown. Your first line must be exactly `Decision: proceed`, `Decision: revise`,
|
|
142
|
+
"Answer in concise Markdown. Your first non-empty line must be exactly `Decision: proceed`, `Decision: revise`, or `Decision: blocked`.",
|
|
59
143
|
"Use blocked only for a critical issue requiring the user. Never claim verification that the supplied evidence does not show.",
|
|
60
144
|
].join(" ");
|
|
61
145
|
|
|
62
|
-
type
|
|
146
|
+
export type GateFailureCategory =
|
|
147
|
+
| "provider-error"
|
|
148
|
+
| "empty-response"
|
|
149
|
+
| "missing-decision"
|
|
150
|
+
| "malformed-decision"
|
|
151
|
+
| "duplicate-decision"
|
|
152
|
+
| "contradictory-decision"
|
|
153
|
+
| "budget-exhausted";
|
|
154
|
+
export interface AdvisorGateFailure {
|
|
155
|
+
category: GateFailureCategory;
|
|
156
|
+
markdown?: string;
|
|
157
|
+
message: string;
|
|
158
|
+
ok: false;
|
|
159
|
+
}
|
|
160
|
+
export interface AdvisorConsultationResult {
|
|
161
|
+
markdown: string;
|
|
162
|
+
model: string;
|
|
163
|
+
thinkingText: string;
|
|
164
|
+
trigger: ConsultationTrigger;
|
|
165
|
+
usage?: unknown;
|
|
166
|
+
}
|
|
167
|
+
export interface AdvisorGateResult {
|
|
168
|
+
decision: GateDecision;
|
|
169
|
+
markdown: string;
|
|
170
|
+
model: string;
|
|
171
|
+
ok: true;
|
|
172
|
+
thinkingText: string;
|
|
173
|
+
trigger: GateTrigger;
|
|
174
|
+
usage?: unknown;
|
|
175
|
+
}
|
|
176
|
+
export type AdvisorGateOutcome = AdvisorGateResult | AdvisorGateFailure;
|
|
63
177
|
|
|
64
|
-
export const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
178
|
+
export const advisorUsageCost = (usage: unknown): number | undefined => {
|
|
179
|
+
const value = usage as
|
|
180
|
+
| { cost?: { total?: unknown }; totalCost?: unknown }
|
|
181
|
+
| undefined;
|
|
182
|
+
const cost = value?.cost?.total ?? value?.totalCost;
|
|
183
|
+
return typeof cost === "number" ? cost : undefined;
|
|
69
184
|
};
|
|
70
185
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
186
|
+
const DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
|
|
187
|
+
const ANY_DECISION_LINE = /^Decision\s*:\s*(.*?)\s*$/i;
|
|
188
|
+
const LINE_BREAK = /\r?\n/;
|
|
189
|
+
|
|
190
|
+
export const parseAutomaticDecision = (
|
|
191
|
+
text: string
|
|
192
|
+
): AdvisorGateResult | AdvisorGateFailure => {
|
|
193
|
+
const lines = text.split(LINE_BREAK);
|
|
194
|
+
const nonEmpty = lines.findIndex((line) => line.trim().length > 0);
|
|
195
|
+
if (nonEmpty === -1) {
|
|
196
|
+
return {
|
|
197
|
+
category: "empty-response",
|
|
198
|
+
message: "Advisor returned an empty gate response.",
|
|
199
|
+
ok: false,
|
|
200
|
+
};
|
|
79
201
|
}
|
|
202
|
+
const first = lines[nonEmpty].trim();
|
|
203
|
+
const match = DECISION_LINE.exec(first);
|
|
204
|
+
if (!match) {
|
|
205
|
+
return {
|
|
206
|
+
category: first.toLowerCase().startsWith("decision:")
|
|
207
|
+
? "malformed-decision"
|
|
208
|
+
: "missing-decision",
|
|
209
|
+
markdown: text,
|
|
210
|
+
message:
|
|
211
|
+
"Advisor gate response must begin with Decision: proceed, Decision: revise, or Decision: blocked.",
|
|
212
|
+
ok: false,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const decision = match[1].toLowerCase() as GateDecision;
|
|
216
|
+
for (const line of lines.slice(nonEmpty + 1)) {
|
|
217
|
+
const subsequent = ANY_DECISION_LINE.exec(line.trim());
|
|
218
|
+
if (!subsequent) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const repeated = subsequent[1].trim().toLowerCase();
|
|
222
|
+
if (repeated === decision) {
|
|
223
|
+
return {
|
|
224
|
+
category: "duplicate-decision",
|
|
225
|
+
markdown: text,
|
|
226
|
+
message: "Advisor gate response contains duplicate decision lines.",
|
|
227
|
+
ok: false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
category: "contradictory-decision",
|
|
232
|
+
markdown: text,
|
|
233
|
+
message: "Advisor gate response contains contradictory decision lines.",
|
|
234
|
+
ok: false,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
decision,
|
|
239
|
+
markdown: text,
|
|
240
|
+
model: "",
|
|
241
|
+
ok: true,
|
|
242
|
+
thinkingText: "",
|
|
243
|
+
trigger: "repeated-tool-call",
|
|
244
|
+
};
|
|
80
245
|
};
|
|
81
246
|
|
|
82
|
-
const adviceForText = (
|
|
83
|
-
`**
|
|
84
|
-
...advice.criticalFindings.map((finding) => `- **${finding.severity}:** ${finding.claim}${finding.evidence ? ` — ${finding.evidence}` : ""}`),
|
|
85
|
-
advice.missingEvidence.length ? `\n**Missing evidence**\n${advice.missingEvidence.map((item) => `- ${item}`).join("\n")}` : "",
|
|
86
|
-
`\n**Smallest next step**\n${advice.smallestNextStep}`,
|
|
87
|
-
advice.verificationRequired.length ? `\n**Required verification**\n${advice.verificationRequired.map((item) => `- ${item}`).join("\n")}` : "",
|
|
88
|
-
].filter(Boolean).join("\n");
|
|
247
|
+
const adviceForText = (result: AdvisorGateResult) =>
|
|
248
|
+
`**Decision: ${result.decision}**\n\n${result.markdown}`;
|
|
89
249
|
|
|
90
|
-
|
|
250
|
+
const collectAdvisorResponse = async (
|
|
91
251
|
ctx: ExtensionContext,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
252
|
+
systemPrompt: string,
|
|
253
|
+
question: string | undefined,
|
|
254
|
+
signal: AbortSignal | undefined,
|
|
255
|
+
onChunk: ((thinking: string, text: string) => void) | undefined
|
|
96
256
|
) => {
|
|
97
257
|
loadConfig(ctx);
|
|
98
258
|
const [provider, modelId] = splitRef(advisorRef);
|
|
99
259
|
const model = ctx.modelRegistry.find(provider, modelId);
|
|
100
|
-
if (!model)
|
|
260
|
+
if (!model) {
|
|
261
|
+
throw new Error(`Advisor model not found: ${advisorRef}`);
|
|
262
|
+
}
|
|
101
263
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
102
|
-
if (!auth.ok)
|
|
103
|
-
|
|
264
|
+
if (!auth.ok) {
|
|
265
|
+
throw new Error((auth as { error: string }).error);
|
|
266
|
+
}
|
|
267
|
+
if (!auth.apiKey) {
|
|
268
|
+
throw new Error(`No API key for ${advisorRef}`);
|
|
269
|
+
}
|
|
104
270
|
|
|
105
271
|
const conversation = recentConversation(ctx, contextMaxCharsRef);
|
|
106
|
-
const messages: Message[] = [
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
272
|
+
const messages: Message[] = [
|
|
273
|
+
{
|
|
274
|
+
content: [
|
|
275
|
+
{ text: advisorMessageText(conversation, question), type: "text" },
|
|
276
|
+
],
|
|
277
|
+
role: "user",
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
},
|
|
280
|
+
];
|
|
111
281
|
|
|
112
282
|
let thinkingText = "";
|
|
113
283
|
let responseText = "";
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
284
|
+
const eventStream = stream(
|
|
285
|
+
model,
|
|
286
|
+
{ messages, systemPrompt },
|
|
287
|
+
{
|
|
288
|
+
apiKey: auth.apiKey,
|
|
289
|
+
env: auth.env,
|
|
290
|
+
headers: auth.headers,
|
|
291
|
+
reasoning: advisorEffortRef as never,
|
|
292
|
+
signal,
|
|
293
|
+
}
|
|
294
|
+
);
|
|
122
295
|
|
|
123
296
|
for await (const event of eventStream) {
|
|
124
297
|
if (event.type === "thinking_delta") {
|
|
@@ -131,182 +304,517 @@ export const consult = async (
|
|
|
131
304
|
}
|
|
132
305
|
|
|
133
306
|
const response = await eventStream.result();
|
|
134
|
-
const lastAssistant = [response].find(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
307
|
+
const lastAssistant = [response].find(
|
|
308
|
+
(m): m is AssistantMessage => m.role === "assistant"
|
|
309
|
+
);
|
|
310
|
+
const markdown =
|
|
311
|
+
lastAssistant?.content
|
|
312
|
+
.filter(
|
|
313
|
+
(part): part is { type: "text"; text: string } => part.type === "text"
|
|
314
|
+
)
|
|
315
|
+
.map((part) => part.text)
|
|
316
|
+
.join("\n") || responseText;
|
|
317
|
+
if (!markdown.trim()) {
|
|
318
|
+
throw new Error("Advisor returned no advice.");
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
markdown,
|
|
322
|
+
model: advisorRef,
|
|
323
|
+
thinkingText,
|
|
324
|
+
usage: (
|
|
325
|
+
lastAssistant as (AssistantMessage & { usage?: unknown }) | undefined
|
|
326
|
+
)?.usage,
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export const consultAdvisor = async (
|
|
331
|
+
ctx: ExtensionContext,
|
|
332
|
+
question?: string,
|
|
333
|
+
signal?: AbortSignal,
|
|
334
|
+
onChunk?: (thinking: string, text: string) => void,
|
|
335
|
+
trigger: ConsultationTrigger = "executor-requested"
|
|
336
|
+
): Promise<AdvisorConsultationResult> => {
|
|
337
|
+
const result = await collectAdvisorResponse(
|
|
338
|
+
ctx,
|
|
339
|
+
ADVISOR_SYSTEM,
|
|
340
|
+
question,
|
|
341
|
+
signal,
|
|
342
|
+
onChunk
|
|
343
|
+
);
|
|
344
|
+
return { ...result, trigger };
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export const runAdvisorGate = async (
|
|
348
|
+
ctx: ExtensionContext,
|
|
349
|
+
question: string,
|
|
350
|
+
trigger: GateTrigger = "repeated-tool-call",
|
|
351
|
+
signal?: AbortSignal,
|
|
352
|
+
onChunk?: (thinking: string, text: string) => void
|
|
353
|
+
): Promise<AdvisorGateOutcome> => {
|
|
354
|
+
try {
|
|
355
|
+
const result = await collectAdvisorResponse(
|
|
356
|
+
ctx,
|
|
357
|
+
ADVISOR_DECISION_SYSTEM,
|
|
358
|
+
question,
|
|
359
|
+
signal,
|
|
360
|
+
onChunk
|
|
361
|
+
);
|
|
362
|
+
const parsed = parseAutomaticDecision(result.markdown);
|
|
363
|
+
if (!parsed.ok) {
|
|
364
|
+
return parsed;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
...parsed,
|
|
368
|
+
model: result.model,
|
|
369
|
+
thinkingText: result.thinkingText,
|
|
370
|
+
trigger,
|
|
371
|
+
usage: result.usage,
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
375
|
+
return {
|
|
376
|
+
category:
|
|
377
|
+
message === "Advisor returned no advice."
|
|
378
|
+
? "empty-response"
|
|
379
|
+
: "provider-error",
|
|
380
|
+
message,
|
|
381
|
+
ok: false,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const notifyLocalFailure = (
|
|
387
|
+
ctx: ExtensionContext,
|
|
388
|
+
message: string,
|
|
389
|
+
sessionBlocked = false
|
|
390
|
+
) => {
|
|
391
|
+
if (ctx.hasUI) {
|
|
392
|
+
ctx.ui.notify(
|
|
393
|
+
`Advisor ${sessionBlocked ? "gate failure; session blocked" : "consultation failed"}: ${message}`,
|
|
394
|
+
"error"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export const gateFailureEffectForMode = (
|
|
400
|
+
mode: "block-session" | "block-tool" | "warn-and-continue"
|
|
401
|
+
) => {
|
|
402
|
+
if (mode === "warn-and-continue") {
|
|
403
|
+
return "continued" as const;
|
|
404
|
+
}
|
|
405
|
+
return mode === "block-tool"
|
|
406
|
+
? ("tool-blocked" as const)
|
|
407
|
+
: ("session-blocked" as const);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const gateDecisionEffect = (decision: GateDecision) => {
|
|
411
|
+
if (decision === "proceed") {
|
|
412
|
+
return "continued" as const;
|
|
413
|
+
}
|
|
414
|
+
return decision === "blocked"
|
|
415
|
+
? ("session-blocked" as const)
|
|
416
|
+
: ("tool-blocked" as const);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const failureEffect = (
|
|
420
|
+
category: GateFailureCategory,
|
|
421
|
+
message: string,
|
|
422
|
+
ctx: ExtensionContext,
|
|
423
|
+
session: AdvisorSessionState
|
|
424
|
+
) => {
|
|
425
|
+
const reason = `Advisor gate ${category}: ${message}`;
|
|
426
|
+
notifyLocalFailure(ctx, message, advisorFailureModeRef === "block-session");
|
|
427
|
+
notifyHerdrAdvisorFailure("Advisor gate failure", reason);
|
|
428
|
+
if (advisorFailureModeRef === "warn-and-continue") {
|
|
429
|
+
return { block: false, effect: "continued" as const, reason };
|
|
430
|
+
}
|
|
431
|
+
if (advisorFailureModeRef === "block-tool") {
|
|
432
|
+
return { block: true, effect: "tool-blocked" as const, reason };
|
|
433
|
+
}
|
|
434
|
+
session.block(reason);
|
|
435
|
+
herdrAdvisorBlock.set(reason);
|
|
436
|
+
if (advisorBlockOnBlockedRef) {
|
|
437
|
+
ctx.abort();
|
|
438
|
+
}
|
|
439
|
+
return { block: true, effect: "session-blocked" as const, reason };
|
|
144
440
|
};
|
|
145
441
|
|
|
146
442
|
export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
147
443
|
const session = advisorSessionState;
|
|
444
|
+
const reservedCalls = new Set<string>();
|
|
148
445
|
|
|
149
|
-
pi.registerMessageRenderer?.(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
446
|
+
pi.registerMessageRenderer?.(
|
|
447
|
+
"advisor-loop-call",
|
|
448
|
+
(message, _options, theme) => {
|
|
449
|
+
const details = message.details as { question?: string } | undefined;
|
|
450
|
+
return renderAdvisorCallBox(details?.question, theme);
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
pi.registerMessageRenderer?.(
|
|
455
|
+
"advisor-loop-result",
|
|
456
|
+
(message, { expanded }, theme) => {
|
|
457
|
+
const details = message.details as
|
|
458
|
+
| { decision?: GateDecision; text?: string; advisor?: string }
|
|
459
|
+
| undefined;
|
|
460
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
461
|
+
box.addChild(
|
|
462
|
+
new Text(
|
|
463
|
+
theme.fg(
|
|
464
|
+
"warning",
|
|
465
|
+
theme.bold(`◆ ADVISOR GATE: ${details?.decision ?? "failure"}`)
|
|
466
|
+
),
|
|
467
|
+
0,
|
|
468
|
+
0
|
|
469
|
+
)
|
|
470
|
+
);
|
|
471
|
+
if (details?.advisor) {
|
|
472
|
+
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
473
|
+
}
|
|
474
|
+
if (details?.text) {
|
|
475
|
+
box.addChild(
|
|
476
|
+
new Markdown(
|
|
477
|
+
adviceForDisplay(details.text, Boolean(expanded)),
|
|
478
|
+
0,
|
|
479
|
+
0,
|
|
480
|
+
getMarkdownTheme()
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
} else {
|
|
484
|
+
box.addChild(
|
|
485
|
+
new Text(
|
|
486
|
+
theme.fg(
|
|
487
|
+
"error",
|
|
488
|
+
typeof message.content === "string"
|
|
489
|
+
? message.content
|
|
490
|
+
: "Advisor gate failed."
|
|
491
|
+
),
|
|
492
|
+
0,
|
|
493
|
+
0
|
|
494
|
+
)
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return box;
|
|
498
|
+
}
|
|
499
|
+
);
|
|
153
500
|
|
|
154
501
|
pi.on("session_start", () => {
|
|
155
502
|
session.resetTask();
|
|
503
|
+
reservedCalls.clear();
|
|
156
504
|
herdrAdvisorBlock.clear();
|
|
157
505
|
});
|
|
158
506
|
|
|
159
507
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
160
|
-
if (!pi.getActiveTools().includes("ask_advisor"))
|
|
508
|
+
if (!pi.getActiveTools().includes("ask_advisor")) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
161
511
|
loadConfig(ctx);
|
|
162
512
|
const guidelines = advisorInvocationGuidelines();
|
|
163
|
-
const budget = session.
|
|
164
|
-
if (budget !== undefined)
|
|
165
|
-
|
|
513
|
+
const budget = session.remainingCalls(advisorMaxCallsPerSessionRef);
|
|
514
|
+
if (budget !== undefined) {
|
|
515
|
+
guidelines.push(
|
|
516
|
+
`Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return guidelines.length > 0
|
|
520
|
+
? {
|
|
521
|
+
systemPrompt: `${ctx.getSystemPrompt()}\n\nAdvisor invocation settings:\n${guidelines.map((rule) => `- ${rule}`).join("\n")}`,
|
|
522
|
+
}
|
|
523
|
+
: undefined;
|
|
166
524
|
});
|
|
167
525
|
|
|
168
526
|
pi.on("tool_call", async (event, ctx) => {
|
|
169
|
-
if (!pi.getActiveTools().includes("ask_advisor"))
|
|
527
|
+
if (!pi.getActiveTools().includes("ask_advisor")) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
170
530
|
loadConfig(ctx);
|
|
171
|
-
if (event.toolName === "ask_advisor"
|
|
172
|
-
|
|
531
|
+
if (event.toolName === "ask_advisor") {
|
|
532
|
+
if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
|
|
533
|
+
const message = "Advisor call budget exhausted for this session.";
|
|
534
|
+
if (ctx.hasUI) {
|
|
535
|
+
ctx.ui.notify(message, "warning");
|
|
536
|
+
}
|
|
537
|
+
notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
|
|
538
|
+
return { block: true, reason: message };
|
|
539
|
+
}
|
|
540
|
+
session.consumeCall();
|
|
541
|
+
reservedCalls.add(event.toolCallId);
|
|
542
|
+
return;
|
|
173
543
|
}
|
|
174
|
-
if (!advisorAutoLoopGateRef)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
544
|
+
if (!advisorAutoLoopGateRef) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
!session.recordToolCall(
|
|
549
|
+
event.toolName,
|
|
550
|
+
event.input,
|
|
551
|
+
advisorLoopThresholdRef
|
|
552
|
+
)
|
|
553
|
+
) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const reason = `Advisor loop gate: normalized signature for ${event.toolName} repeated ${advisorLoopThresholdRef} times without a materially different tool action.`;
|
|
557
|
+
if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
|
|
558
|
+
const failure = failureEffect(
|
|
559
|
+
"budget-exhausted",
|
|
560
|
+
"Advisor gate call budget is exhausted.",
|
|
561
|
+
ctx,
|
|
562
|
+
session
|
|
563
|
+
);
|
|
564
|
+
return failure.block
|
|
565
|
+
? { block: true, reason: failure.reason }
|
|
566
|
+
: undefined;
|
|
567
|
+
}
|
|
568
|
+
session.consumeCall();
|
|
569
|
+
herdrAdvisorActivity.start();
|
|
570
|
+
pi.sendMessage(
|
|
571
|
+
{
|
|
183
572
|
content: "Automatic Advisor loop review",
|
|
573
|
+
customType: "advisor-loop-call",
|
|
574
|
+
details: {
|
|
575
|
+
question: `Loop gate: ${event.toolName} repeated ${advisorLoopThresholdRef} times`,
|
|
576
|
+
},
|
|
184
577
|
display: true,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
578
|
+
},
|
|
579
|
+
{ deliverAs: "steer" }
|
|
580
|
+
);
|
|
581
|
+
try {
|
|
582
|
+
const result = await runAdvisorGate(
|
|
583
|
+
ctx,
|
|
584
|
+
`${reason} Review the repeated actions and recommend the smallest safe next step.`
|
|
585
|
+
);
|
|
586
|
+
if (!result.ok) {
|
|
587
|
+
session.recordInvocation({
|
|
588
|
+
executionEffect: gateFailureEffectForMode(advisorFailureModeRef),
|
|
589
|
+
failure: result.category,
|
|
590
|
+
kind: "gate",
|
|
591
|
+
model: advisorRef,
|
|
592
|
+
trigger: "repeated-tool-call",
|
|
593
|
+
});
|
|
594
|
+
const failure = failureEffect(
|
|
595
|
+
result.category,
|
|
596
|
+
result.message,
|
|
597
|
+
ctx,
|
|
598
|
+
session
|
|
599
|
+
);
|
|
600
|
+
return failure.block
|
|
601
|
+
? { block: true, reason: `${reason}\n${failure.reason}` }
|
|
602
|
+
: undefined;
|
|
603
|
+
}
|
|
604
|
+
session.recordInvocation({
|
|
605
|
+
cost: advisorUsageCost(result.usage),
|
|
606
|
+
decision: result.decision,
|
|
607
|
+
executionEffect: gateDecisionEffect(result.decision),
|
|
608
|
+
kind: "gate",
|
|
609
|
+
model: result.model,
|
|
610
|
+
trigger: result.trigger,
|
|
611
|
+
usage: result.usage,
|
|
612
|
+
});
|
|
613
|
+
pi.sendMessage(
|
|
614
|
+
{
|
|
615
|
+
content: adviceForText(result),
|
|
616
|
+
customType: "advisor-loop-result",
|
|
617
|
+
details: {
|
|
618
|
+
advisor: result.model,
|
|
619
|
+
decision: result.decision,
|
|
620
|
+
text: result.markdown,
|
|
621
|
+
},
|
|
193
622
|
display: true,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
623
|
+
},
|
|
624
|
+
{ deliverAs: "steer" }
|
|
625
|
+
);
|
|
626
|
+
if (result.decision === "proceed") {
|
|
627
|
+
session.resetRepetition();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const gateReason = `Advisor loop review: ${result.markdown}`;
|
|
631
|
+
if (result.decision === "blocked") {
|
|
632
|
+
session.block(gateReason);
|
|
633
|
+
herdrAdvisorBlock.set(gateReason);
|
|
634
|
+
if (advisorBlockOnBlockedRef) {
|
|
635
|
+
ctx.abort();
|
|
207
636
|
}
|
|
208
|
-
} catch (error) {
|
|
209
|
-
session.recordConsultation("automatic");
|
|
210
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
-
reason = `${reason}\nAdvisor loop review failed: ${message}`;
|
|
212
|
-
session.block(reason);
|
|
213
|
-
herdrAdvisorBlock.set(reason);
|
|
214
|
-
blockSession = true;
|
|
215
|
-
if (ctx.hasUI) ctx.ui.notify(`Advisor loop review failed. Session is blocked: ${message}`, "error");
|
|
216
|
-
} finally {
|
|
217
|
-
herdrAdvisorActivity.finish();
|
|
218
637
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
herdrAdvisorBlock.set(reason);
|
|
223
|
-
blockSession = true;
|
|
224
|
-
if (ctx.hasUI) ctx.ui.notify("Advisor loop review was not run because the session call budget is exhausted. Session is blocked.", "error");
|
|
638
|
+
return { block: true, reason: gateReason };
|
|
639
|
+
} finally {
|
|
640
|
+
herdrAdvisorActivity.finish();
|
|
225
641
|
}
|
|
226
|
-
if (blockSession && advisorBlockOnBlockedRef) ctx.abort();
|
|
227
|
-
return { block: true, reason };
|
|
228
642
|
});
|
|
229
643
|
|
|
230
644
|
pi.on("agent_settled", (_event, ctx) => {
|
|
231
|
-
if (session.blocked || !advisorSessionSummaryRef)
|
|
645
|
+
if (session.blocked || !advisorSessionSummaryRef) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
232
648
|
const summary = session.summary(advisorMaxCallsPerSessionRef);
|
|
233
|
-
if (summary && ctx.hasUI)
|
|
649
|
+
if (summary && ctx.hasUI) {
|
|
650
|
+
ctx.ui.notify(summary, "info");
|
|
651
|
+
}
|
|
234
652
|
});
|
|
235
653
|
|
|
236
|
-
pi.on("session_shutdown", () =>
|
|
654
|
+
pi.on("session_shutdown", () => {
|
|
655
|
+
reservedCalls.clear();
|
|
656
|
+
herdrAdvisorBlock.clear();
|
|
657
|
+
});
|
|
237
658
|
|
|
238
659
|
pi.registerTool({
|
|
239
|
-
|
|
660
|
+
description:
|
|
661
|
+
"Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a context-aware review; add question only for a genuinely targeted focus.",
|
|
662
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
663
|
+
if (!reservedCalls.delete(_id)) {
|
|
664
|
+
if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
|
|
665
|
+
throw new Error("Advisor call budget exhausted for this session.");
|
|
666
|
+
}
|
|
667
|
+
session.consumeCall();
|
|
668
|
+
}
|
|
669
|
+
herdrAdvisorActivity.start();
|
|
670
|
+
try {
|
|
671
|
+
const result = await consultAdvisor(
|
|
672
|
+
ctx,
|
|
673
|
+
resolveAdvisorRequest(params.question),
|
|
674
|
+
signal,
|
|
675
|
+
(t, tx) =>
|
|
676
|
+
onUpdate?.({
|
|
677
|
+
content: [{ text: tx, type: "text" }],
|
|
678
|
+
details: {
|
|
679
|
+
advisor: advisorRef,
|
|
680
|
+
question: resolveAdvisorRequest(params.question),
|
|
681
|
+
text: tx,
|
|
682
|
+
thinking: t,
|
|
683
|
+
},
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
session.recordInvocation({
|
|
687
|
+
cost: advisorUsageCost(result.usage),
|
|
688
|
+
executionEffect: "continued",
|
|
689
|
+
kind: "markdown",
|
|
690
|
+
model: result.model,
|
|
691
|
+
trigger: "executor-requested",
|
|
692
|
+
usage: result.usage,
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
content: [
|
|
696
|
+
{
|
|
697
|
+
text: `Advisor (${result.model})\n\n${result.markdown}`,
|
|
698
|
+
type: "text",
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
details: {
|
|
702
|
+
advisor: result.model,
|
|
703
|
+
question: resolveAdvisorRequest(params.question),
|
|
704
|
+
text: result.markdown,
|
|
705
|
+
thinking: result.thinkingText,
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
} catch (error) {
|
|
709
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
710
|
+
session.recordInvocation({
|
|
711
|
+
executionEffect: "continued",
|
|
712
|
+
failure: "provider-error",
|
|
713
|
+
kind: "markdown",
|
|
714
|
+
model: advisorRef,
|
|
715
|
+
trigger: "executor-requested",
|
|
716
|
+
});
|
|
717
|
+
notifyLocalFailure(ctx, message);
|
|
718
|
+
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
719
|
+
throw error;
|
|
720
|
+
} finally {
|
|
721
|
+
herdrAdvisorActivity.finish();
|
|
722
|
+
}
|
|
723
|
+
},
|
|
240
724
|
label: "Ask Advisor",
|
|
241
|
-
|
|
242
|
-
|
|
725
|
+
name: "ask_advisor",
|
|
726
|
+
parameters: Type.Object({
|
|
727
|
+
question: Type.Optional(
|
|
728
|
+
Type.String({
|
|
729
|
+
description:
|
|
730
|
+
"The specific question or decision to get advice on. Omit this for normal reviews: the Advisor already has the conversation context.",
|
|
731
|
+
})
|
|
732
|
+
),
|
|
733
|
+
}),
|
|
243
734
|
promptGuidelines: [
|
|
244
|
-
"Call ask_advisor with an empty object by default. Do not invent a question merely to request a review: the Advisor already receives
|
|
735
|
+
"Call ask_advisor with an empty object by default. Do not invent a question merely to request a review: the Advisor already receives context. Include question only for a genuinely specific assumption or trade-off.",
|
|
245
736
|
],
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
renderCall(args, theme
|
|
737
|
+
promptSnippet:
|
|
738
|
+
"Consult the Advisor using its existing context; omit question unless a specific focus is necessary",
|
|
739
|
+
renderCall(args, theme) {
|
|
249
740
|
return renderAdvisorCallBox(args.question?.trim(), theme);
|
|
250
741
|
},
|
|
251
742
|
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
252
|
-
const box =
|
|
743
|
+
const box =
|
|
744
|
+
context.lastComponent instanceof Box
|
|
745
|
+
? context.lastComponent
|
|
746
|
+
: new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
253
747
|
box.setBgFn((text) => theme.bg("customMessageBg", text));
|
|
254
748
|
box.clear();
|
|
255
749
|
if (isPartial) {
|
|
256
750
|
if (!context.state.timerId) {
|
|
257
751
|
context.state.timerId = setInterval(() => context.invalidate(), 80);
|
|
258
752
|
}
|
|
259
|
-
const frame =
|
|
260
|
-
|
|
261
|
-
const
|
|
753
|
+
const frame =
|
|
754
|
+
SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length];
|
|
755
|
+
const d = result.details as
|
|
756
|
+
| { thinking?: string; text?: string }
|
|
757
|
+
| undefined;
|
|
758
|
+
const lines: string[] = [
|
|
759
|
+
`${theme.fg("warning", theme.bold(`◆ ADVISOR ${frame}`))} ${theme.fg("dim", "· Working…")}`,
|
|
760
|
+
];
|
|
262
761
|
if (d?.thinking) {
|
|
263
|
-
|
|
264
|
-
|
|
762
|
+
lines.push(
|
|
763
|
+
theme.fg(
|
|
764
|
+
"thinkingText",
|
|
765
|
+
` 💭 ${(d.thinking.length > 200 ? d.thinking.slice(-200) : d.thinking).replace(/\n/g, " ")}`
|
|
766
|
+
)
|
|
767
|
+
);
|
|
265
768
|
}
|
|
266
769
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
267
|
-
if (d?.text)
|
|
770
|
+
if (d?.text) {
|
|
771
|
+
box.addChild(
|
|
772
|
+
new Markdown(
|
|
773
|
+
adviceForDisplay(d.text, Boolean(expanded)),
|
|
774
|
+
0,
|
|
775
|
+
0,
|
|
776
|
+
getMarkdownTheme()
|
|
777
|
+
)
|
|
778
|
+
);
|
|
779
|
+
}
|
|
268
780
|
} else {
|
|
269
781
|
if (context.state.timerId) {
|
|
270
782
|
clearInterval(context.state.timerId);
|
|
271
|
-
|
|
783
|
+
context.state.timerId = undefined;
|
|
784
|
+
}
|
|
785
|
+
const d = result.details as
|
|
786
|
+
| { thinking?: string; text?: string; advisor?: string }
|
|
787
|
+
| undefined;
|
|
788
|
+
const lines: string[] = [
|
|
789
|
+
theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE")),
|
|
790
|
+
];
|
|
791
|
+
if (d?.advisor) {
|
|
792
|
+
lines.push(theme.fg("dim", ` ${d.advisor}`));
|
|
272
793
|
}
|
|
273
|
-
const d = result.details as { thinking?: string; text?: string; advisor?: string } | undefined;
|
|
274
|
-
const lines: string[] = [theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE"))];
|
|
275
|
-
if (d?.advisor) lines.push(theme.fg("dim", ` ${d.advisor}`));
|
|
276
794
|
if (d?.thinking) {
|
|
277
|
-
lines.push(
|
|
795
|
+
lines.push(
|
|
796
|
+
theme.fg(
|
|
797
|
+
"thinkingText",
|
|
798
|
+
` 💭 ${d.thinking.replace(/\n/g, " ").slice(0, 300)}${d.thinking.length > 300 ? "…" : ""}`
|
|
799
|
+
)
|
|
800
|
+
);
|
|
278
801
|
}
|
|
279
|
-
const advice =
|
|
802
|
+
const advice =
|
|
803
|
+
d?.text ||
|
|
804
|
+
textFrom(result.content) ||
|
|
805
|
+
"(Advisor returned no advice.)";
|
|
280
806
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
281
|
-
box.addChild(
|
|
807
|
+
box.addChild(
|
|
808
|
+
new Markdown(
|
|
809
|
+
adviceForDisplay(advice, Boolean(expanded)),
|
|
810
|
+
0,
|
|
811
|
+
0,
|
|
812
|
+
getMarkdownTheme()
|
|
813
|
+
)
|
|
814
|
+
);
|
|
282
815
|
}
|
|
283
816
|
return box;
|
|
284
817
|
},
|
|
285
|
-
|
|
286
|
-
const question = resolveAdvisorRequest(params.question);
|
|
287
|
-
herdrAdvisorActivity.start();
|
|
288
|
-
try {
|
|
289
|
-
session.consumeAutomaticCall();
|
|
290
|
-
const { advice, thinkingText, structured } = await consult(ctx, question, signal, (t, tx) => {
|
|
291
|
-
onUpdate?.({
|
|
292
|
-
content: [{ type: "text", text: tx }],
|
|
293
|
-
details: { thinking: t, text: tx, advisor: advisorRef, question },
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
session.recordConsultation("executor", structured.verdict);
|
|
297
|
-
if (structured.verdict === "blocked") {
|
|
298
|
-
const reason = structured.escalationReason || structured.smallestNextStep;
|
|
299
|
-
session.block(reason);
|
|
300
|
-
herdrAdvisorBlock.set(reason);
|
|
301
|
-
if (advisorBlockOnBlockedRef) ctx.abort();
|
|
302
|
-
}
|
|
303
|
-
return {
|
|
304
|
-
content: [{ type: "text", text: `Advisor (${advisorRef})\n\n${advice}` }],
|
|
305
|
-
details: { thinking: thinkingText, text: advice, advisor: advisorRef, question, verdict: structured.verdict },
|
|
306
|
-
};
|
|
307
|
-
} finally {
|
|
308
|
-
herdrAdvisorActivity.finish();
|
|
309
|
-
}
|
|
310
|
-
},
|
|
818
|
+
renderShell: "self",
|
|
311
819
|
});
|
|
312
820
|
};
|