pi-sage 0.2.13 → 0.2.15
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.
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CallerContext } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type InputSource = "interactive" | "rpc" | "extension";
|
|
4
|
+
export type InteractiveOrRpcSource = "interactive" | "rpc";
|
|
5
|
+
|
|
6
|
+
export function isKnownInputSource(value: string): value is InputSource {
|
|
7
|
+
return value === "interactive" || value === "rpc" || value === "extension";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function updateLastInteractiveOrRpcSource(
|
|
11
|
+
previous: InteractiveOrRpcSource | undefined,
|
|
12
|
+
source: string
|
|
13
|
+
): InteractiveOrRpcSource | undefined {
|
|
14
|
+
if (source === "interactive" || source === "rpc") {
|
|
15
|
+
return source;
|
|
16
|
+
}
|
|
17
|
+
return previous;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildCallerContextFromSignals(input: {
|
|
21
|
+
lastInteractiveOrRpcSource: InteractiveOrRpcSource | undefined;
|
|
22
|
+
unknownSourceSeen: boolean;
|
|
23
|
+
hasUI: boolean;
|
|
24
|
+
roleHint?: string;
|
|
25
|
+
isCI?: boolean;
|
|
26
|
+
isSubagent?: boolean;
|
|
27
|
+
}): CallerContext | null {
|
|
28
|
+
const { lastInteractiveOrRpcSource, unknownSourceSeen, hasUI, roleHint, isCI, isSubagent } = input;
|
|
29
|
+
if (unknownSourceSeen) return null;
|
|
30
|
+
if (!lastInteractiveOrRpcSource) return null;
|
|
31
|
+
|
|
32
|
+
const normalizedRole = roleHint?.trim().toLowerCase();
|
|
33
|
+
const isRpcSource = lastInteractiveOrRpcSource === "rpc";
|
|
34
|
+
const interactive = hasUI && lastInteractiveOrRpcSource === "interactive";
|
|
35
|
+
const isInteractiveSupervisor = interactive && normalizedRole === "supervisor";
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
session: {
|
|
39
|
+
interactive
|
|
40
|
+
},
|
|
41
|
+
agent: {
|
|
42
|
+
role: normalizedRole ?? "primary",
|
|
43
|
+
isSubagent: Boolean(isSubagent),
|
|
44
|
+
isRpcOrchestrated: isRpcSource || Boolean(normalizedRole && normalizedRole !== "primary" && !isInteractiveSupervisor)
|
|
45
|
+
},
|
|
46
|
+
runtime: {
|
|
47
|
+
mode: isCI ? "ci" : hasUI ? (isRpcSource ? "rpc" : "interactive") : "non-interactive"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -7,14 +7,26 @@ import {
|
|
|
7
7
|
isHardCostCapExceeded,
|
|
8
8
|
makeBlockedResult
|
|
9
9
|
} from "./policy.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildCallerContextFromSignals,
|
|
12
|
+
isKnownInputSource,
|
|
13
|
+
updateLastInteractiveOrRpcSource,
|
|
14
|
+
type InteractiveOrRpcSource
|
|
15
|
+
} from "./caller-context.js";
|
|
16
|
+
import {
|
|
17
|
+
isSageRunnerPolicyError,
|
|
18
|
+
runSageSingleShot,
|
|
19
|
+
type SageRunnerInput,
|
|
20
|
+
type SageRunnerOutput
|
|
21
|
+
} from "./runner.js";
|
|
11
22
|
import {
|
|
12
23
|
getSettingsPathForScope,
|
|
13
24
|
loadSettings,
|
|
14
25
|
mergeSettings,
|
|
15
26
|
saveSettings,
|
|
16
27
|
type SageSettings,
|
|
17
|
-
type SettingsScope
|
|
28
|
+
type SettingsScope,
|
|
29
|
+
type ToolPolicySettings
|
|
18
30
|
} from "./settings.js";
|
|
19
31
|
import { checkVolumeCaps, getDisallowedCustomTools, resolveToolPolicy } from "./tool-policy.js";
|
|
20
32
|
import type {
|
|
@@ -28,8 +40,6 @@ import type {
|
|
|
28
40
|
ToolProfile
|
|
29
41
|
} from "./types.js";
|
|
30
42
|
|
|
31
|
-
type InputSource = "interactive" | "rpc" | "extension";
|
|
32
|
-
|
|
33
43
|
type ModelLike = {
|
|
34
44
|
provider: string;
|
|
35
45
|
id: string;
|
|
@@ -68,12 +78,22 @@ export default function registerSageExtension(pi: ExtensionAPI): void {
|
|
|
68
78
|
sessionCostTotal: 0
|
|
69
79
|
};
|
|
70
80
|
|
|
71
|
-
let
|
|
81
|
+
let lastInteractiveOrRpcSource: InteractiveOrRpcSource | undefined;
|
|
82
|
+
let unknownInputSourceSeen = false;
|
|
72
83
|
|
|
73
84
|
pi.on("input", (event) => {
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
const source = String(event.source ?? "");
|
|
86
|
+
|
|
87
|
+
if (!isKnownInputSource(source)) {
|
|
88
|
+
unknownInputSourceSeen = true;
|
|
89
|
+
return { action: "continue" };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lastInteractiveOrRpcSource = updateLastInteractiveOrRpcSource(lastInteractiveOrRpcSource, source);
|
|
93
|
+
if (source === "interactive" || source === "rpc") {
|
|
94
|
+
unknownInputSourceSeen = false;
|
|
76
95
|
}
|
|
96
|
+
|
|
77
97
|
return { action: "continue" };
|
|
78
98
|
});
|
|
79
99
|
|
|
@@ -160,7 +180,7 @@ export default function registerSageExtension(pi: ExtensionAPI): void {
|
|
|
160
180
|
});
|
|
161
181
|
}
|
|
162
182
|
|
|
163
|
-
const callerContext = buildCallerContext(
|
|
183
|
+
const callerContext = buildCallerContext(lastInteractiveOrRpcSource, unknownInputSourceSeen, ctx.hasUI);
|
|
164
184
|
const eligibility = isEligibleCaller(callerContext);
|
|
165
185
|
if (!eligibility.ok) {
|
|
166
186
|
return makeBlockedResult({
|
|
@@ -232,10 +252,8 @@ export default function registerSageExtension(pi: ExtensionAPI): void {
|
|
|
232
252
|
});
|
|
233
253
|
}
|
|
234
254
|
|
|
235
|
-
const toolPolicy = resolveToolPolicy(settings.toolPolicy);
|
|
236
|
-
|
|
237
255
|
try {
|
|
238
|
-
const
|
|
256
|
+
const runnerInput: Omit<SageRunnerInput, "toolPolicy"> = {
|
|
239
257
|
cwd: ctx.cwd,
|
|
240
258
|
model: resolvedModel,
|
|
241
259
|
reasoningLevel: settings.reasoningLevel,
|
|
@@ -246,11 +264,13 @@ export default function registerSageExtension(pi: ExtensionAPI): void {
|
|
|
246
264
|
evidence: input.evidence,
|
|
247
265
|
objective: input.objective,
|
|
248
266
|
urgency: input.urgency,
|
|
249
|
-
toolPolicy: settings.toolPolicy,
|
|
250
267
|
signal
|
|
251
|
-
}
|
|
268
|
+
};
|
|
252
269
|
|
|
253
|
-
const
|
|
270
|
+
const invocation = await runSageWithFallback(runnerInput, settings.toolPolicy);
|
|
271
|
+
const toolPolicy = resolveToolPolicy(invocation.toolPolicyUsed);
|
|
272
|
+
|
|
273
|
+
const volume = checkVolumeCaps(invocation.result.toolUsage, toolPolicy);
|
|
254
274
|
if (!volume.ok) {
|
|
255
275
|
return makeBlockedResult({
|
|
256
276
|
mode,
|
|
@@ -266,21 +286,25 @@ export default function registerSageExtension(pi: ExtensionAPI): void {
|
|
|
266
286
|
budgetState.callsThisTurn += 1;
|
|
267
287
|
budgetState.sessionCalls += 1;
|
|
268
288
|
if (mode === "autonomous") budgetState.lastAutoTurn = budgetState.currentTurn;
|
|
269
|
-
if (
|
|
289
|
+
if (invocation.result.usage?.costTotal) budgetState.sessionCostTotal += invocation.result.usage.costTotal;
|
|
290
|
+
|
|
291
|
+
const contextReason = invocation.fallbackReason
|
|
292
|
+
? "eligible; fallback to read-only-lite after blocked bash command"
|
|
293
|
+
: "eligible";
|
|
270
294
|
|
|
271
295
|
return {
|
|
272
|
-
content: [{ type: "text", text:
|
|
296
|
+
content: [{ type: "text", text: invocation.result.text }],
|
|
273
297
|
details: {
|
|
274
298
|
model: resolvedModel,
|
|
275
299
|
reasoningLevel: settings.reasoningLevel,
|
|
276
|
-
latencyMs:
|
|
277
|
-
stopReason:
|
|
278
|
-
usage:
|
|
279
|
-
toolUsage:
|
|
300
|
+
latencyMs: invocation.result.latencyMs,
|
|
301
|
+
stopReason: invocation.result.stopReason,
|
|
302
|
+
usage: invocation.result.usage,
|
|
303
|
+
toolUsage: invocation.result.toolUsage,
|
|
280
304
|
policy: {
|
|
281
305
|
mode,
|
|
282
306
|
allowedByContext: true,
|
|
283
|
-
contextReason
|
|
307
|
+
contextReason,
|
|
284
308
|
allowedByBudget: true,
|
|
285
309
|
budgetReason: softBudget.reason
|
|
286
310
|
}
|
|
@@ -353,28 +377,45 @@ function formatRunnerPolicyReason(blockCode: BlockCode, reason: string): string
|
|
|
353
377
|
return reason;
|
|
354
378
|
}
|
|
355
379
|
|
|
356
|
-
function
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
380
|
+
async function runSageWithFallback(
|
|
381
|
+
runnerInput: Omit<SageRunnerInput, "toolPolicy">,
|
|
382
|
+
toolPolicy: ToolPolicySettings
|
|
383
|
+
): Promise<{ result: SageRunnerOutput; toolPolicyUsed: ToolPolicySettings; fallbackReason?: string }> {
|
|
384
|
+
try {
|
|
385
|
+
const result = await runSageSingleShot({ ...runnerInput, toolPolicy });
|
|
386
|
+
return { result, toolPolicyUsed: toolPolicy };
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (
|
|
389
|
+
isSageRunnerPolicyError(error) &&
|
|
390
|
+
error.blockCode === "tool-disallowed" &&
|
|
391
|
+
toolPolicy.profile === "git-review-readonly"
|
|
392
|
+
) {
|
|
393
|
+
const fallbackPolicy: ToolPolicySettings = {
|
|
394
|
+
...toolPolicy,
|
|
395
|
+
profile: "read-only-lite"
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const result = await runSageSingleShot({ ...runnerInput, toolPolicy: fallbackPolicy });
|
|
399
|
+
return { result, toolPolicyUsed: fallbackPolicy, fallbackReason: error.message };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
364
405
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
};
|
|
406
|
+
function buildCallerContext(
|
|
407
|
+
lastInteractiveOrRpcSource: InteractiveOrRpcSource | undefined,
|
|
408
|
+
unknownInputSourceSeen: boolean,
|
|
409
|
+
hasUI: boolean
|
|
410
|
+
): CallerContext | null {
|
|
411
|
+
return buildCallerContextFromSignals({
|
|
412
|
+
lastInteractiveOrRpcSource,
|
|
413
|
+
unknownSourceSeen: unknownInputSourceSeen,
|
|
414
|
+
hasUI,
|
|
415
|
+
roleHint: getRoleHint(),
|
|
416
|
+
isCI: Boolean(process.env.CI),
|
|
417
|
+
isSubagent: process.env.PI_SAGE_SUBAGENT === "1"
|
|
418
|
+
});
|
|
378
419
|
}
|
|
379
420
|
|
|
380
421
|
function getRoleHint(): string | undefined {
|