pi-advisor-flow 0.1.8 → 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.
@@ -1,76 +1,217 @@
1
- export type AdvisorVerdict = "proceed" | "revise" | "blocked" | "insufficient-evidence";
2
- export type ConsultationOrigin = "executor" | "automatic" | "manual";
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
- type Consultation = { origin: ConsultationOrigin; verdict?: AdvisorVerdict };
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 stableJson = (value: unknown): string => {
7
- if (value === null || typeof value !== "object") return JSON.stringify(value);
8
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
9
- const record = value as Record<string, unknown>;
10
- return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
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
- #consultations: Consultation[] = [];
104
+ #invocations: AdvisorInvocationRecord[] = [];
18
105
  #loopInterventions = 0;
19
- #automaticCalls = 0;
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.#consultations = [];
112
+ this.#invocations = [];
26
113
  this.#loopInterventions = 0;
27
- this.#automaticCalls = 0;
114
+ this.#consumedCalls = 0;
28
115
  }
29
116
 
30
- clearBlocked() { this.#blockedReason = undefined; }
31
- resetRepetition() { this.#previousSignature = undefined; this.#repetitions = 0; }
32
- get blocked() { return this.#blockedReason !== undefined; }
33
- get blockedReason() { return this.#blockedReason; }
34
- block(reason: string) { this.#blockedReason ??= reason; }
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") return false;
38
- const signature = `${toolName}:${stableJson(input)}`;
39
- this.#repetitions = signature === this.#previousSignature ? this.#repetitions + 1 : 1;
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) return false;
142
+ if (this.#repetitions < threshold) {
143
+ return false;
144
+ }
42
145
  this.#loopInterventions += 1;
43
146
  return true;
44
147
  }
45
148
 
46
- canUseAutomaticCall(limit: number | undefined) {
47
- return limit === undefined || this.#automaticCalls < limit;
149
+ canConsult(limit: number | undefined) {
150
+ return limit === undefined || this.#consumedCalls < limit;
48
151
  }
49
-
50
- consumeAutomaticCall() { this.#automaticCalls += 1; }
51
- remainingAutomaticCalls(limit: number | undefined) {
52
- return limit === undefined ? undefined : Math.max(0, limit - this.#automaticCalls);
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
- recordConsultation(origin: ConsultationOrigin, verdict?: AdvisorVerdict) {
56
- this.#consultations.push({ origin, verdict });
164
+ recordInvocation(record: AdvisorInvocationRecord) {
165
+ this.#invocations.push(record);
57
166
  }
58
167
 
59
168
  summary(limit: number | undefined) {
60
- if (this.#consultations.length === 0 && this.#loopInterventions === 0) return undefined;
61
- const count = (origin: ConsultationOrigin) => this.#consultations.filter((item) => item.origin === origin).length;
62
- const verdicts = (["proceed", "revise", "insufficient-evidence", "blocked"] as AdvisorVerdict[])
63
- .map((verdict) => [verdict, this.#consultations.filter((item) => item.verdict === verdict).length] as const)
64
- .filter(([, count]) => count > 0)
65
- .map(([verdict, count]) => `${count} ${verdict}`)
66
- .join(", ") || "none recorded";
67
- const budget = limit === undefined ? "unlimited" : `${this.#automaticCalls} / ${limit} used`;
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: ${this.#consultations.length} ${count("executor")} Executor-requested, ${count("automatic")} automatic, ${count("manual")} manual`,
71
- `Verdicts: ${verdicts}`,
72
- `Loop interventions: ${this.#loopInterventions}`,
73
- `Automatic budget: ${budget}`,
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
  }