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/session-state.ts
CHANGED
|
@@ -1,76 +1,217 @@
|
|
|
1
|
-
export type
|
|
2
|
-
export type
|
|
1
|
+
export type GateDecision = "proceed" | "revise" | "blocked";
|
|
2
|
+
export type ConsultationTrigger = "manual" | "executor-requested";
|
|
3
|
+
export type GateTrigger =
|
|
4
|
+
| "repeated-tool-call"
|
|
5
|
+
| "completion-review"
|
|
6
|
+
| "custom-rule";
|
|
7
|
+
export type AdvisorTrigger = ConsultationTrigger | GateTrigger;
|
|
8
|
+
export type ExecutionEffect = "continued" | "tool-blocked" | "session-blocked";
|
|
3
9
|
|
|
4
|
-
|
|
10
|
+
export interface AdvisorInvocationRecord {
|
|
11
|
+
cost?: number;
|
|
12
|
+
decision?: GateDecision;
|
|
13
|
+
executionEffect: ExecutionEffect;
|
|
14
|
+
failure?: string;
|
|
15
|
+
kind: "markdown" | "gate";
|
|
16
|
+
model: string;
|
|
17
|
+
trigger: AdvisorTrigger;
|
|
18
|
+
usage?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const WHITESPACE = /\s/;
|
|
22
|
+
const TIMESTAMP_KEY = /timestamp|time|date/;
|
|
23
|
+
const REQUEST_ID_KEY = /requestid|correlationid|traceid/;
|
|
24
|
+
const isVolatileKey = (key: string, pattern: RegExp) =>
|
|
25
|
+
pattern.test(key.replace(/[-_]/g, "").toLowerCase());
|
|
5
26
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
const normalizeShellWhitespace = (command: string) => {
|
|
28
|
+
let result = "";
|
|
29
|
+
let quote: "'" | '"' | "`" | undefined;
|
|
30
|
+
let pendingSpace = false;
|
|
31
|
+
for (const char of command.trim()) {
|
|
32
|
+
if (quote) {
|
|
33
|
+
result += char;
|
|
34
|
+
if (char === quote) {
|
|
35
|
+
quote = undefined;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
40
|
+
if (pendingSpace && result) {
|
|
41
|
+
result += " ";
|
|
42
|
+
}
|
|
43
|
+
pendingSpace = false;
|
|
44
|
+
quote = char;
|
|
45
|
+
result += char;
|
|
46
|
+
} else if (WHITESPACE.test(char)) {
|
|
47
|
+
pendingSpace = true;
|
|
48
|
+
} else {
|
|
49
|
+
if (pendingSpace && result) {
|
|
50
|
+
result += " ";
|
|
51
|
+
}
|
|
52
|
+
pendingSpace = false;
|
|
53
|
+
result += char;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
11
57
|
};
|
|
12
58
|
|
|
59
|
+
const normalizeString = (value: string) =>
|
|
60
|
+
value
|
|
61
|
+
.replace(/\/(?:private\/)?tmp\/[^\s/]+/g, "/tmp/<temporary>")
|
|
62
|
+
.replace(/\/var\/folders\/[^\s/]+/g, "/var/folders/<temporary>");
|
|
63
|
+
|
|
64
|
+
export const normalizeToolInput = (
|
|
65
|
+
toolName: string,
|
|
66
|
+
input: unknown
|
|
67
|
+
): unknown => {
|
|
68
|
+
const visit = (value: unknown, key?: string): unknown => {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
if (key && isVolatileKey(key, TIMESTAMP_KEY)) {
|
|
71
|
+
return "<timestamp>";
|
|
72
|
+
}
|
|
73
|
+
if (key && isVolatileKey(key, REQUEST_ID_KEY)) {
|
|
74
|
+
return "<request-id>";
|
|
75
|
+
}
|
|
76
|
+
const normalized = normalizeString(value);
|
|
77
|
+
return toolName === "bash" && key === "command"
|
|
78
|
+
? normalizeShellWhitespace(normalized)
|
|
79
|
+
: normalized;
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
return value.map((item) => visit(item));
|
|
83
|
+
}
|
|
84
|
+
if (value && typeof value === "object") {
|
|
85
|
+
const record = value as Record<string, unknown>;
|
|
86
|
+
return Object.fromEntries(
|
|
87
|
+
Object.keys(record)
|
|
88
|
+
.sort()
|
|
89
|
+
.map((childKey) => [childKey, visit(record[childKey], childKey)])
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
};
|
|
94
|
+
return visit(input);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const normalizedToolSignature = (toolName: string, input: unknown) =>
|
|
98
|
+
`${toolName}:${JSON.stringify(normalizeToolInput(toolName, input))}`;
|
|
99
|
+
|
|
13
100
|
export class AdvisorSessionState {
|
|
14
101
|
#previousSignature?: string;
|
|
15
102
|
#repetitions = 0;
|
|
16
103
|
#blockedReason?: string;
|
|
17
|
-
#
|
|
104
|
+
#invocations: AdvisorInvocationRecord[] = [];
|
|
18
105
|
#loopInterventions = 0;
|
|
19
|
-
#
|
|
106
|
+
#consumedCalls = 0;
|
|
20
107
|
|
|
21
108
|
resetTask() {
|
|
22
109
|
this.#previousSignature = undefined;
|
|
23
110
|
this.#repetitions = 0;
|
|
24
111
|
this.#blockedReason = undefined;
|
|
25
|
-
this.#
|
|
112
|
+
this.#invocations = [];
|
|
26
113
|
this.#loopInterventions = 0;
|
|
27
|
-
this.#
|
|
114
|
+
this.#consumedCalls = 0;
|
|
28
115
|
}
|
|
29
116
|
|
|
30
|
-
clearBlocked() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
117
|
+
clearBlocked() {
|
|
118
|
+
this.#blockedReason = undefined;
|
|
119
|
+
}
|
|
120
|
+
resetRepetition() {
|
|
121
|
+
this.#previousSignature = undefined;
|
|
122
|
+
this.#repetitions = 0;
|
|
123
|
+
}
|
|
124
|
+
get blocked() {
|
|
125
|
+
return this.#blockedReason !== undefined;
|
|
126
|
+
}
|
|
127
|
+
get blockedReason() {
|
|
128
|
+
return this.#blockedReason;
|
|
129
|
+
}
|
|
130
|
+
block(reason: string) {
|
|
131
|
+
this.#blockedReason ??= reason;
|
|
132
|
+
}
|
|
35
133
|
|
|
36
134
|
recordToolCall(toolName: string, input: unknown, threshold: number) {
|
|
37
|
-
if (toolName === "ask_advisor")
|
|
38
|
-
|
|
39
|
-
|
|
135
|
+
if (toolName === "ask_advisor") {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const signature = normalizedToolSignature(toolName, input);
|
|
139
|
+
this.#repetitions =
|
|
140
|
+
signature === this.#previousSignature ? this.#repetitions + 1 : 1;
|
|
40
141
|
this.#previousSignature = signature;
|
|
41
|
-
if (this.#repetitions < threshold)
|
|
142
|
+
if (this.#repetitions < threshold) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
42
145
|
this.#loopInterventions += 1;
|
|
43
146
|
return true;
|
|
44
147
|
}
|
|
45
148
|
|
|
46
|
-
|
|
47
|
-
return limit === undefined || this.#
|
|
149
|
+
canConsult(limit: number | undefined) {
|
|
150
|
+
return limit === undefined || this.#consumedCalls < limit;
|
|
48
151
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
152
|
+
consumeCall() {
|
|
153
|
+
this.#consumedCalls += 1;
|
|
154
|
+
}
|
|
155
|
+
remainingCalls(limit: number | undefined) {
|
|
156
|
+
return limit === undefined
|
|
157
|
+
? undefined
|
|
158
|
+
: Math.max(0, limit - this.#consumedCalls);
|
|
159
|
+
}
|
|
160
|
+
get consumedCalls() {
|
|
161
|
+
return this.#consumedCalls;
|
|
53
162
|
}
|
|
54
163
|
|
|
55
|
-
|
|
56
|
-
this.#
|
|
164
|
+
recordInvocation(record: AdvisorInvocationRecord) {
|
|
165
|
+
this.#invocations.push(record);
|
|
57
166
|
}
|
|
58
167
|
|
|
59
168
|
summary(limit: number | undefined) {
|
|
60
|
-
if (this.#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
169
|
+
if (this.#invocations.length === 0 && this.#loopInterventions === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const markdown = this.#invocations.filter(
|
|
173
|
+
(item) => item.kind === "markdown"
|
|
174
|
+
);
|
|
175
|
+
const gates = this.#invocations.filter((item) => item.kind === "gate");
|
|
176
|
+
const countTrigger = (trigger: AdvisorTrigger) =>
|
|
177
|
+
this.#invocations.filter((item) => item.trigger === trigger).length;
|
|
178
|
+
const decisions =
|
|
179
|
+
(["proceed", "revise", "blocked"] as GateDecision[])
|
|
180
|
+
.map(
|
|
181
|
+
(decision) =>
|
|
182
|
+
[
|
|
183
|
+
decision,
|
|
184
|
+
gates.filter((item) => item.decision === decision).length,
|
|
185
|
+
] as const
|
|
186
|
+
)
|
|
187
|
+
.filter(([, count]) => count > 0)
|
|
188
|
+
.map(([decision, count]) => `${count} ${decision}`)
|
|
189
|
+
.join(", ") || "none";
|
|
190
|
+
const effects = (effect: ExecutionEffect) =>
|
|
191
|
+
this.#invocations.filter((item) => item.executionEffect === effect)
|
|
192
|
+
.length;
|
|
193
|
+
const failures = this.#invocations
|
|
194
|
+
.filter((item) => item.failure)
|
|
195
|
+
.map((item) => item.failure);
|
|
196
|
+
const models =
|
|
197
|
+
[
|
|
198
|
+
...new Set(this.#invocations.map((item) => item.model).filter(Boolean)),
|
|
199
|
+
].join(", ") || "unknown";
|
|
200
|
+
const budget =
|
|
201
|
+
limit === undefined
|
|
202
|
+
? `${this.#consumedCalls} used; unlimited remaining`
|
|
203
|
+
: `${this.#consumedCalls} / ${limit} used; ${Math.max(0, limit - this.#consumedCalls)} remaining`;
|
|
68
204
|
return [
|
|
69
205
|
"[Session Advisor Summary]",
|
|
70
|
-
`Consultations: ${
|
|
71
|
-
`
|
|
72
|
-
`
|
|
73
|
-
`
|
|
206
|
+
`Consultations: ${markdown.length} Markdown (${countTrigger("manual")} manual, ${countTrigger("executor-requested")} executor-requested), automatic gates: ${gates.length}`,
|
|
207
|
+
`Triggers: ${["manual", "executor-requested", "repeated-tool-call", "completion-review", "custom-rule"].filter((trigger) => countTrigger(trigger as AdvisorTrigger) > 0).join(", ") || "none"}`,
|
|
208
|
+
`Models: ${models}`,
|
|
209
|
+
`Budget: ${budget}`,
|
|
210
|
+
`Markdown advice: ${markdown.length} responses`,
|
|
211
|
+
`Gate decisions: ${decisions}`,
|
|
212
|
+
`Loop matching: normalized tool signatures; ${this.#loopInterventions} gate intervention${this.#loopInterventions === 1 ? "" : "s"}`,
|
|
213
|
+
`Execution effects: ${effects("tool-blocked")} tool blocked, ${effects("session-blocked")} sessions blocked, ${effects("continued")} continued`,
|
|
214
|
+
`Failures: ${failures.length ? failures.join(", ") : "none"}`,
|
|
74
215
|
].join("\n");
|
|
75
216
|
}
|
|
76
217
|
}
|