@caupulican/pi-adaptative 0.80.99 → 0.80.102
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 +203 -0
- package/dist/core/agent-session.d.ts +72 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +437 -46
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/envelope-enforcement.d.ts +5 -0
- package/dist/core/autonomy/envelope-enforcement.d.ts.map +1 -1
- package/dist/core/autonomy/envelope-enforcement.js +28 -4
- package/dist/core/autonomy/envelope-enforcement.js.map +1 -1
- package/dist/core/autonomy/path-scope.d.ts.map +1 -1
- package/dist/core/autonomy/path-scope.js +33 -2
- package/dist/core/autonomy/path-scope.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +15 -7
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/context/brain-curator.d.ts +10 -0
- package/dist/core/context/brain-curator.d.ts.map +1 -1
- package/dist/core/context/brain-curator.js +19 -2
- package/dist/core/context/brain-curator.js.map +1 -1
- package/dist/core/context-gc.d.ts +8 -3
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +5 -0
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/delegation/worker-actions.d.ts +0 -10
- package/dist/core/delegation/worker-actions.d.ts.map +1 -1
- package/dist/core/delegation/worker-actions.js +5 -7
- package/dist/core/delegation/worker-actions.js.map +1 -1
- package/dist/core/delegation/worker-result.d.ts +6 -0
- package/dist/core/delegation/worker-result.d.ts.map +1 -1
- package/dist/core/delegation/worker-result.js +10 -4
- package/dist/core/delegation/worker-result.js.map +1 -1
- package/dist/core/delegation/worker-runner.d.ts +3 -0
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +6 -1
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/doctor.d.ts +66 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +144 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/learning/learning-audit.d.ts +13 -1
- package/dist/core/learning/learning-audit.d.ts.map +1 -1
- package/dist/core/learning/learning-audit.js +22 -1
- package/dist/core/learning/learning-audit.js.map +1 -1
- package/dist/core/learning/learning-gate.d.ts +3 -0
- package/dist/core/learning/learning-gate.d.ts.map +1 -1
- package/dist/core/learning/learning-gate.js +13 -4
- package/dist/core/learning/learning-gate.js.map +1 -1
- package/dist/core/models/default-model-suggestions.d.ts +34 -0
- package/dist/core/models/default-model-suggestions.d.ts.map +1 -0
- package/dist/core/models/default-model-suggestions.js +58 -0
- package/dist/core/models/default-model-suggestions.js.map +1 -0
- package/dist/core/models/local-registration.d.ts +2 -0
- package/dist/core/models/local-registration.d.ts.map +1 -1
- package/dist/core/models/local-registration.js +2 -1
- package/dist/core/models/local-registration.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts +76 -2
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +233 -21
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/profile-resource-selection.d.ts +15 -3
- package/dist/core/profile-resource-selection.d.ts.map +1 -1
- package/dist/core/profile-resource-selection.js +19 -9
- package/dist/core/profile-resource-selection.js.map +1 -1
- package/dist/core/resource-loader.d.ts +20 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +42 -16
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +30 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +65 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +14 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +8 -0
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/fff-search-backend.d.ts +34 -1
- package/dist/core/tools/fff-search-backend.d.ts.map +1 -1
- package/dist/core/tools/fff-search-backend.js +43 -4
- package/dist/core/tools/fff-search-backend.js.map +1 -1
- package/dist/core/tools/find.d.ts +16 -0
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +17 -3
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +20 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +15 -3
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/fitness-role-selector.d.ts +6 -2
- package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/fitness-role-selector.js +55 -45
- package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-suggestion-selector.d.ts +13 -0
- package/dist/modes/interactive/components/model-suggestion-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/model-suggestion-selector.js +40 -0
- package/dist/modes/interactive/components/model-suggestion-selector.js.map +1 -0
- package/dist/modes/interactive/components/profile-resource-editor.d.ts +3 -0
- package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/profile-resource-editor.js +16 -2
- package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +98 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +8 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +57 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/safe-write-stream.d.ts +8 -0
- package/dist/utils/safe-write-stream.d.ts.map +1 -1
- package/dist/utils/safe-write-stream.js +32 -0
- package/dist/utils/safe-write-stream.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +92 -2
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +172 -32
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -36
- package/package.json +4 -4
|
@@ -58,7 +58,7 @@ import { GatewayRegistry } from "./gateways/channel-provider.js";
|
|
|
58
58
|
import { buildGoalContinuationPrompt, } from "./goals/goal-continuation-prompt.js";
|
|
59
59
|
import { buildGoalRuntimeSnapshot, } from "./goals/goal-runtime-snapshot.js";
|
|
60
60
|
import { appendGoalStateSnapshot, getLatestGoalStateSnapshot } from "./goals/session-goal-state.js";
|
|
61
|
-
import { appendLearningAuditSnapshot, getLearningAuditSnapshots, proposalFromReflectionWrite, rollbackPlanForReflectionWrite, } from "./learning/learning-audit.js";
|
|
61
|
+
import { APPLY_WRITE_REFUSED_REASON_CODE, appendLearningAuditSnapshot, contradictionsForReflectionWrite, getLearningAuditSnapshots, proposalFromReflectionWrite, rollbackPlanForReflectionWrite, } from "./learning/learning-audit.js";
|
|
62
62
|
import { evaluateLearningDecision } from "./learning/learning-gate.js";
|
|
63
63
|
import { ObservationStore, observationKey } from "./learning/observation-store.js";
|
|
64
64
|
import { decideDemand, ReflectionEngine, } from "./learning/reflection-engine.js";
|
|
@@ -80,6 +80,8 @@ import { bufferModelRouterSessionCustomMessage, bufferModelRouterSessionMessage,
|
|
|
80
80
|
import { formatModelRouterStatus, getRecentModelRouterDecisions, MODEL_ROUTER_DECISION_CUSTOM_TYPE, } from "./model-router/status.js";
|
|
81
81
|
import { shouldEscalateModelRouterTool } from "./model-router/tool-escalation.js";
|
|
82
82
|
import { FitnessStore } from "./models/fitness-store.js";
|
|
83
|
+
import { OLLAMA_PROVIDER } from "./models/local-registration.js";
|
|
84
|
+
import { OllamaRuntime } from "./models/local-runtime.js";
|
|
83
85
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
84
86
|
import { runModelFitnessProbe } from "./research/model-fitness.js";
|
|
85
87
|
import { runResearch } from "./research/research-runner.js";
|
|
@@ -153,6 +155,12 @@ function persistModelRouterDecision(sessionManager, decision) {
|
|
|
153
155
|
const AUTONOMY_TELEMETRY_CUSTOM_TYPE = "autonomy-telemetry";
|
|
154
156
|
/** G8: bound on the in-memory gate-outcome history. Oldest entries evict once the cap is reached. */
|
|
155
157
|
const GATE_OUTCOME_HISTORY_LIMIT = 50;
|
|
158
|
+
/** User-facing router tiers in ascending order — "learning" is never selected for a user turn, so
|
|
159
|
+
* it has no place in the escalation ladder (#27's _ensureRouteModelReady walks this forward only). */
|
|
160
|
+
const MODEL_ROUTER_TIER_ORDER = ["cheap", "medium", "expensive"];
|
|
161
|
+
/** How long the #31 "install ollama now?" confirm waits before auto-dismissing (same as a "No") —
|
|
162
|
+
* long enough to read and decide, short enough that an unattended session doesn't hang a turn on it. */
|
|
163
|
+
const OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS = 30_000;
|
|
156
164
|
/** Read a packed grep/find tool result's `details.artifactId`, if present, without `any`. */
|
|
157
165
|
function extractArtifactId(message) {
|
|
158
166
|
if (!message || message.role !== "toolResult")
|
|
@@ -239,6 +247,8 @@ export class AgentSession {
|
|
|
239
247
|
_brainCurator = new BrainCurator();
|
|
240
248
|
_lastCurationSkipReason = undefined;
|
|
241
249
|
_inertExtensionWarnings = [];
|
|
250
|
+
/** Extensions the active resource profile removed from the runtime set (surfaced in /context). */
|
|
251
|
+
_profileDeniedExtensionCount = 0;
|
|
242
252
|
_lastPreDigestSkipReason = undefined;
|
|
243
253
|
_unboundToolGrantWarnings = [];
|
|
244
254
|
_toolArtifactStore = undefined;
|
|
@@ -268,6 +278,12 @@ export class AgentSession {
|
|
|
268
278
|
_cwd;
|
|
269
279
|
_agentDir;
|
|
270
280
|
_collectWorkspaceSources;
|
|
281
|
+
_localRuntimeDeps;
|
|
282
|
+
/** Lazy, cached by baseUrl so the router path and any other caller share one instance per server. */
|
|
283
|
+
_localRuntimes = new Map();
|
|
284
|
+
/** Server URLs confirmed reachable THIS session — skips the health-check round trip on every
|
|
285
|
+
* local-routed turn once warm. Keyed the same way as _localRuntimes. */
|
|
286
|
+
_localRuntimeConfirmedUp = new Set();
|
|
271
287
|
_extensionRunnerRef;
|
|
272
288
|
_initialActiveToolNames;
|
|
273
289
|
_allowedToolNames;
|
|
@@ -338,6 +354,7 @@ export class AgentSession {
|
|
|
338
354
|
this._cwd = config.cwd;
|
|
339
355
|
this._agentDir = config.agentDir ?? getAgentDir();
|
|
340
356
|
this._collectWorkspaceSources = config.collectWorkspaceSources ?? collectWorkspaceSources;
|
|
357
|
+
this._localRuntimeDeps = config.localRuntimeDeps;
|
|
341
358
|
this._modelRegistry = config.modelRegistry;
|
|
342
359
|
this._extensionRunnerRef = config.extensionRunnerRef;
|
|
343
360
|
this._initialActiveToolNames = config.initialActiveToolNames;
|
|
@@ -1018,6 +1035,7 @@ export class AgentSession {
|
|
|
1018
1035
|
adjustments: { memoryEvidenceTokens, enforcementSavedTokens },
|
|
1019
1036
|
extraObservations: [
|
|
1020
1037
|
...this._resourceLoader.getAgentsDiagnostics().map((diagnostic) => diagnostic.message),
|
|
1038
|
+
...this._profileDeniedResourceObservations(),
|
|
1021
1039
|
...this._inertExtensionWarnings,
|
|
1022
1040
|
...this._unboundToolGrantWarnings,
|
|
1023
1041
|
// G7: auto-built per-turn foreground envelope (observe-only; not enforced). Falls back to a
|
|
@@ -1441,6 +1459,16 @@ export class AgentSession {
|
|
|
1441
1459
|
}
|
|
1442
1460
|
// Track last assistant message for auto-compaction check
|
|
1443
1461
|
_lastAssistantMessage = undefined;
|
|
1462
|
+
/**
|
|
1463
|
+
* User messages already painted to the UI by an early, synthetic `message_start` fired from
|
|
1464
|
+
* `_promptUnserialized` — before the model-router judge's bounded LLM call — so the prompt
|
|
1465
|
+
* appears immediately instead of hanging until routing finishes. The real agent-loop run emits
|
|
1466
|
+
* its own authoritative `message_start` for the SAME message object once the turn actually
|
|
1467
|
+
* starts; `_handleAgentEvent` consumes (deletes) it from this set to suppress that one duplicate
|
|
1468
|
+
* listener notification. Persistence is untouched: it stays keyed off `message_end`, which is
|
|
1469
|
+
* never added here and never suppressed.
|
|
1470
|
+
*/
|
|
1471
|
+
_earlyDisplayedUserMessages = new Set();
|
|
1444
1472
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
1445
1473
|
_handleAgentEvent = async (event) => {
|
|
1446
1474
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
@@ -1470,8 +1498,14 @@ export class AgentSession {
|
|
|
1470
1498
|
const suppressRetryPromptEvent = this._isModelRouterRetry &&
|
|
1471
1499
|
(event.type === "message_start" || event.type === "message_end") &&
|
|
1472
1500
|
(event.message.role === "user" || event.message.role === "custom");
|
|
1501
|
+
// This is the authoritative message_start for a user message already painted early (see
|
|
1502
|
+
// _promptUnserialized). Set#delete both tests and consumes membership in one step, so only
|
|
1503
|
+
// this one duplicate is suppressed and a later, unrelated user message is never affected.
|
|
1504
|
+
const suppressAlreadyDisplayedUserMessage = event.type === "message_start" &&
|
|
1505
|
+
event.message.role === "user" &&
|
|
1506
|
+
this._earlyDisplayedUserMessages.delete(event.message);
|
|
1473
1507
|
// Notify all listeners
|
|
1474
|
-
if (!suppressRetryPromptEvent) {
|
|
1508
|
+
if (!suppressRetryPromptEvent && !suppressAlreadyDisplayedUserMessage) {
|
|
1475
1509
|
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
|
1476
1510
|
}
|
|
1477
1511
|
// Handle session/context retention. Tool result details are UI/log metadata,
|
|
@@ -2021,7 +2055,7 @@ export class AgentSession {
|
|
|
2021
2055
|
- Treat current-turn evidence as a cue, not proof; prefer longitudinal corroboration before changing durable behavior.
|
|
2022
2056
|
- Active-task work remains primary: learning runs must not interrupt user-visible execution or claim task completion.`;
|
|
2023
2057
|
}
|
|
2024
|
-
|
|
2058
|
+
_buildSystemPromptOptionsForToolNames(toolNames) {
|
|
2025
2059
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
2026
2060
|
const toolSnippets = {};
|
|
2027
2061
|
const promptGuidelines = [];
|
|
@@ -2054,7 +2088,7 @@ export class AgentSession {
|
|
|
2054
2088
|
// to invoke) a skill its profile blocks.
|
|
2055
2089
|
const loadedSkills = this._resourceLoader.getActiveSkills();
|
|
2056
2090
|
const loadedContextFiles = this._resourceLoader.getAgentsFiles().agentsFiles;
|
|
2057
|
-
|
|
2091
|
+
return {
|
|
2058
2092
|
cwd: this._cwd,
|
|
2059
2093
|
skills: loadedSkills,
|
|
2060
2094
|
contextFiles: loadedContextFiles,
|
|
@@ -2065,8 +2099,21 @@ export class AgentSession {
|
|
|
2065
2099
|
promptGuidelines,
|
|
2066
2100
|
extensions: [...this._extensionRunner.activeExtensions],
|
|
2067
2101
|
};
|
|
2102
|
+
}
|
|
2103
|
+
_rebuildSystemPrompt(toolNames) {
|
|
2104
|
+
this._baseSystemPromptOptions = this._buildSystemPromptOptionsForToolNames(toolNames);
|
|
2068
2105
|
return buildSystemPrompt(this._baseSystemPromptOptions);
|
|
2069
2106
|
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Build a system prompt for a specific tool surface WITHOUT touching the session's base prompt
|
|
2109
|
+
* state. Used for a router-swapped turn (G4): the routed model runs against a filtered tool set,
|
|
2110
|
+
* so it must also receive a system prompt whose tool guidelines/snippets match that filtered
|
|
2111
|
+
* surface — but the change is per-turn, so it must not mutate `_baseSystemPromptOptions` (which
|
|
2112
|
+
* later turns and extension events read).
|
|
2113
|
+
*/
|
|
2114
|
+
_buildSystemPromptForToolNames(toolNames) {
|
|
2115
|
+
return buildSystemPrompt(this._buildSystemPromptOptionsForToolNames(toolNames));
|
|
2116
|
+
}
|
|
2070
2117
|
// =========================================================================
|
|
2071
2118
|
// Prompting
|
|
2072
2119
|
// =========================================================================
|
|
@@ -2180,6 +2227,190 @@ export class AgentSession {
|
|
|
2180
2227
|
return undefined;
|
|
2181
2228
|
}
|
|
2182
2229
|
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Shared {@link OllamaRuntime} for a given server, lazily created and cached by baseUrl so every
|
|
2232
|
+
* caller — the router's readiness gate below and any host UI's own model-lifecycle commands
|
|
2233
|
+
* (e.g. `/models`) — sees and can stop the SAME pi-managed process instead of each tracking its
|
|
2234
|
+
* own untracked child.
|
|
2235
|
+
*/
|
|
2236
|
+
getLocalRuntime(baseUrl) {
|
|
2237
|
+
const key = baseUrl ?? "default";
|
|
2238
|
+
let runtime = this._localRuntimes.get(key);
|
|
2239
|
+
if (!runtime) {
|
|
2240
|
+
runtime = new OllamaRuntime({ agentDir: this._agentDir, baseUrl, deps: this._localRuntimeDeps });
|
|
2241
|
+
this._localRuntimes.set(key, runtime);
|
|
2242
|
+
}
|
|
2243
|
+
return runtime;
|
|
2244
|
+
}
|
|
2245
|
+
/** models.json registers a local model's baseUrl as `<server>/v1` (OpenAI-compat); the runtime's
|
|
2246
|
+
* own health/boot endpoints are on the Ollama-native server root. */
|
|
2247
|
+
_deriveOllamaServerUrl(modelBaseUrl) {
|
|
2248
|
+
return modelBaseUrl.replace(/\/v1\/?$/, "");
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* If the last assistant message in this session was an error from THIS exact local server, a
|
|
2252
|
+
* cached "confirmed up" flag would be stale (the server may have died mid-session) — drop it so
|
|
2253
|
+
* the next ensure-check is a real one instead of trusting stale state.
|
|
2254
|
+
*/
|
|
2255
|
+
_invalidateLocalRuntimeIfLastCallFailed(model, serverUrl) {
|
|
2256
|
+
const lastAssistant = this._findLastAssistantMessage();
|
|
2257
|
+
if (lastAssistant?.stopReason === "error" &&
|
|
2258
|
+
lastAssistant.provider === OLLAMA_PROVIDER &&
|
|
2259
|
+
lastAssistant.model === model.id) {
|
|
2260
|
+
this._localRuntimeConfirmedUp.delete(serverUrl);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Ensure a routed model is actually reachable before the turn calls it. No-op (and free) for any
|
|
2265
|
+
* non-local model — this only ever does network/process work for the `ollama` provider. Caches a
|
|
2266
|
+
* "confirmed up this session" flag per server so a steady-state session pays the health-check
|
|
2267
|
+
* round trip once, not on every turn; invalidated above when a prior local call actually failed,
|
|
2268
|
+
* so a server that died mid-session gets re-detected rather than trusted forever. Boots via
|
|
2269
|
+
* `startReuseExisting()` — never owned storage — so the turn sees the user's OWN pulled models,
|
|
2270
|
+
* the same server `/models` commands and the user's own `ollama` CLI already talk to. Never
|
|
2271
|
+
* installs anything itself (installGuide is GUIDE MODE: printed, never executed).
|
|
2272
|
+
*/
|
|
2273
|
+
async _ensureLocalModelReady(model) {
|
|
2274
|
+
if (model.provider !== OLLAMA_PROVIDER) {
|
|
2275
|
+
return { ready: true, reason: "not_local" };
|
|
2276
|
+
}
|
|
2277
|
+
const serverUrl = this._deriveOllamaServerUrl(model.baseUrl);
|
|
2278
|
+
this._invalidateLocalRuntimeIfLastCallFailed(model, serverUrl);
|
|
2279
|
+
if (this._localRuntimeConfirmedUp.has(serverUrl)) {
|
|
2280
|
+
return { ready: true, reason: "confirmed_up_cached" };
|
|
2281
|
+
}
|
|
2282
|
+
const runtime = this.getLocalRuntime(serverUrl);
|
|
2283
|
+
const status = await runtime.detect();
|
|
2284
|
+
if (status.serverUp) {
|
|
2285
|
+
this._localRuntimeConfirmedUp.add(serverUrl);
|
|
2286
|
+
return { ready: true, reason: "already_running" };
|
|
2287
|
+
}
|
|
2288
|
+
if (!status.binaryPath) {
|
|
2289
|
+
return { ready: false, reason: "binary_missing", installGuide: runtime.installGuide() };
|
|
2290
|
+
}
|
|
2291
|
+
const started = await runtime.startReuseExisting();
|
|
2292
|
+
if (started.started) {
|
|
2293
|
+
this._localRuntimeConfirmedUp.add(serverUrl);
|
|
2294
|
+
}
|
|
2295
|
+
return { ready: started.started, reason: started.reason };
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* #31: the ONE case a routed local model's unreadiness can be fixed automatically is a missing
|
|
2299
|
+
* ollama binary — an unreachable server can't be helped by installing, so that reason is left to
|
|
2300
|
+
* the graceful-fallback warning below unchanged. Only offered when there's an interactive UI to
|
|
2301
|
+
* ask through: headless/RPC/print sessions have no _extensionUIContext and fall straight through,
|
|
2302
|
+
* same as declining or timing out (both resolve confirm() to false). Reverses "pi never runs
|
|
2303
|
+
* installers itself" specifically for this one path — the user is asked first, the download is
|
|
2304
|
+
* pi's own (never curl|sh), and it lands in pi's own runtimes dir (see OllamaRuntime.installManaged).
|
|
2305
|
+
*
|
|
2306
|
+
* Pauses/resumes the routing working-indicator around the confirm dialog itself (re-emitting
|
|
2307
|
+
* routing_end/routing_start — both already idempotent, see interactive-mode.ts's handlers) so an
|
|
2308
|
+
* animated spinner doesn't fight a dialog the user is trying to read and answer; the indicator
|
|
2309
|
+
* comes back for the download/extract that follows a "yes", which is genuine processing feedback.
|
|
2310
|
+
*/
|
|
2311
|
+
async _maybeInstallOllamaOnConsent(model, readiness) {
|
|
2312
|
+
const ui = this._extensionUIContext;
|
|
2313
|
+
if (!ui || readiness.ready || readiness.reason !== "binary_missing")
|
|
2314
|
+
return readiness;
|
|
2315
|
+
const modelLabel = formatModelRouterModel(model);
|
|
2316
|
+
this._emit({ type: "routing_end" });
|
|
2317
|
+
let confirmed;
|
|
2318
|
+
try {
|
|
2319
|
+
confirmed = await ui.confirm("Install Ollama?", `Ollama isn't installed, so the local model "${modelLabel}" can't run. Pi can download and ` +
|
|
2320
|
+
"install it now (a large one-time download, possibly over 1 GB depending on your platform) " +
|
|
2321
|
+
"into its own runtimes folder — never curl|sh, never touching anything outside pi's own " +
|
|
2322
|
+
"directory. Install it now?", { timeout: OLLAMA_INSTALL_CONFIRM_TIMEOUT_MS });
|
|
2323
|
+
}
|
|
2324
|
+
finally {
|
|
2325
|
+
this._emit({ type: "routing_start" });
|
|
2326
|
+
}
|
|
2327
|
+
if (!confirmed)
|
|
2328
|
+
return readiness;
|
|
2329
|
+
const serverUrl = this._deriveOllamaServerUrl(model.baseUrl);
|
|
2330
|
+
const runtime = this.getLocalRuntime(serverUrl);
|
|
2331
|
+
let installResult;
|
|
2332
|
+
try {
|
|
2333
|
+
installResult = await runtime.installManaged((status) => ui.setStatus("ollama-install", status));
|
|
2334
|
+
}
|
|
2335
|
+
finally {
|
|
2336
|
+
ui.setStatus("ollama-install", undefined);
|
|
2337
|
+
}
|
|
2338
|
+
if (!installResult.ok) {
|
|
2339
|
+
return { ready: false, reason: "install_failed", installAttemptError: installResult.error };
|
|
2340
|
+
}
|
|
2341
|
+
return this._ensureLocalModelReady(model);
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Router-swap gate (#27): a turn routed to a local model (any tier, including an executor-direct
|
|
2345
|
+
* route — both carry tier "cheap") must not dead-end the turn just because ollama isn't up.
|
|
2346
|
+
* Never a SILENT swap: every fallback is announced in a warning that states (i) the local model
|
|
2347
|
+
* was unavailable and WHY — binary missing surfaces the install guide inline; any other reason
|
|
2348
|
+
* gets a "check that ollama is running" hint — and (ii) which tier is now handling the turn, so
|
|
2349
|
+
* the cost shift is never a surprise. Escalates cheap -> medium -> expensive, skipping any
|
|
2350
|
+
* unconfigured intermediate tier, reusing the router's own existing "model unavailable"
|
|
2351
|
+
* resolution (_resolveConfiguredTierModel) rather than inventing a new fallback mechanism.
|
|
2352
|
+
* Escalation is bounded: tier strictly increases each hop, so it terminates within two hops.
|
|
2353
|
+
*
|
|
2354
|
+
* Before the warning/escalation below: #31's consent gate gets one shot at fixing a missing
|
|
2355
|
+
* binary interactively (see _maybeInstallOllamaOnConsent) — declining, timing out, running
|
|
2356
|
+
* headless, or the install attempt itself failing all fall through here unchanged, just with an
|
|
2357
|
+
* honest reason (an install that failed is worded as a failed install, not re-labeled as if
|
|
2358
|
+
* nothing was ever tried).
|
|
2359
|
+
*/
|
|
2360
|
+
async _ensureRouteModelReady(resolved) {
|
|
2361
|
+
let current = resolved;
|
|
2362
|
+
while (current && current.model.provider === OLLAMA_PROVIDER) {
|
|
2363
|
+
let readiness = await this._ensureLocalModelReady(current.model);
|
|
2364
|
+
if (!readiness.ready) {
|
|
2365
|
+
readiness = await this._maybeInstallOllamaOnConsent(current.model, readiness);
|
|
2366
|
+
}
|
|
2367
|
+
if (readiness.ready)
|
|
2368
|
+
return current;
|
|
2369
|
+
// Walk the remaining tiers in order (never back down to cheap) and take the first one that
|
|
2370
|
+
// actually resolves — an unconfigured intermediate tier (e.g. no mediumModel set) must be
|
|
2371
|
+
// skipped, not treated as "no fallback available".
|
|
2372
|
+
const startIndex = MODEL_ROUTER_TIER_ORDER.indexOf(current.decision.tier);
|
|
2373
|
+
let escalated;
|
|
2374
|
+
for (let i = startIndex + 1; startIndex !== -1 && i < MODEL_ROUTER_TIER_ORDER.length; i++) {
|
|
2375
|
+
const tier = MODEL_ROUTER_TIER_ORDER[i];
|
|
2376
|
+
const model = this._resolveConfiguredTierModel(tier);
|
|
2377
|
+
if (model) {
|
|
2378
|
+
escalated = { tier, model };
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
const modelLabel = formatModelRouterModel(current.model);
|
|
2383
|
+
const whyText = readiness.installAttemptError
|
|
2384
|
+
? `pi tried to install it just now, but the install attempt failed: ${readiness.installAttemptError}`
|
|
2385
|
+
: readiness.installGuide
|
|
2386
|
+
? ["the ollama binary is not installed.", ...readiness.installGuide].join("\n")
|
|
2387
|
+
: `its server is not reachable (${readiness.reason}) — check that ollama is running.`;
|
|
2388
|
+
const fallbackText = escalated
|
|
2389
|
+
? `Falling back to the ${escalated.tier} tier for this turn.`
|
|
2390
|
+
: "No other tier is configured — falling back to the session's default model.";
|
|
2391
|
+
this._emit({
|
|
2392
|
+
type: "warning",
|
|
2393
|
+
message: `Local model "${modelLabel}" is unavailable: ${whyText}\n${fallbackText}`,
|
|
2394
|
+
});
|
|
2395
|
+
if (!escalated)
|
|
2396
|
+
return undefined; // no higher tier resolves — caller falls back to the session default
|
|
2397
|
+
current = {
|
|
2398
|
+
model: escalated.model,
|
|
2399
|
+
decision: {
|
|
2400
|
+
...current.decision,
|
|
2401
|
+
tier: escalated.tier,
|
|
2402
|
+
fallbackFrom: current.decision.tier,
|
|
2403
|
+
reasonCode: "local_model_not_ready_fallback",
|
|
2404
|
+
reasons: [
|
|
2405
|
+
...current.decision.reasons,
|
|
2406
|
+
`Local model not ready (${readiness.reason}); escalated to ${escalated.tier}`,
|
|
2407
|
+
],
|
|
2408
|
+
model: formatModelRouterModel(escalated.model),
|
|
2409
|
+
},
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
return current;
|
|
2413
|
+
}
|
|
2183
2414
|
_resolveModelRouterTurnRoute(prompt) {
|
|
2184
2415
|
const settings = this.settingsManager.getModelRouterSettings();
|
|
2185
2416
|
if (!settings.enabled) {
|
|
@@ -2300,7 +2531,9 @@ export class AgentSession {
|
|
|
2300
2531
|
systemPrompt,
|
|
2301
2532
|
messages: [{ role: "user", content: [{ type: "text", text: userPrompt }], timestamp: Date.now() }],
|
|
2302
2533
|
model: judgeModel,
|
|
2303
|
-
|
|
2534
|
+
// Per-tier thinking (R1): judgeThinking overrides the judge's own completion; unset
|
|
2535
|
+
// keeps today's "off" (the judge is a cheap classification call by default).
|
|
2536
|
+
thinkingLevel: settings.judgeThinking ?? "off",
|
|
2304
2537
|
maxTokens: ROUTE_JUDGE_MAX_OUTPUT_TOKENS,
|
|
2305
2538
|
signal,
|
|
2306
2539
|
// The judge system prompt is static — the provider can cache the prefix.
|
|
@@ -2370,6 +2603,12 @@ export class AgentSession {
|
|
|
2370
2603
|
const previousModel = this.agent.state.model;
|
|
2371
2604
|
const previousThinkingLevel = this.agent.state.thinkingLevel;
|
|
2372
2605
|
const previousTurnTools = this.agent.state.tools;
|
|
2606
|
+
const previousSystemPrompt = this.agent.state.systemPrompt;
|
|
2607
|
+
// G4 swap bookkeeping (Bug G): the exact references the swap below assigns, so the finally can
|
|
2608
|
+
// restore ONLY what IT put there — never assigned when no swap happens (e.g. a full-class
|
|
2609
|
+
// routed profile).
|
|
2610
|
+
let swappedTools;
|
|
2611
|
+
let swappedSystemPrompt;
|
|
2373
2612
|
const previousActiveModelRouterIntent = this._activeModelRouterIntent;
|
|
2374
2613
|
const previousActiveModelRouterRoute = this._activeModelRouterRoute;
|
|
2375
2614
|
const previousModelRouterSessionBuffer = this._modelRouterSessionBuffer;
|
|
@@ -2401,7 +2640,24 @@ export class AgentSession {
|
|
|
2401
2640
|
}
|
|
2402
2641
|
if (!modelsAreEqual(this.model, routedModel)) {
|
|
2403
2642
|
this.agent.state.model = routedModel;
|
|
2404
|
-
|
|
2643
|
+
// Per-tier thinking (R1): a configured tier/executor thinking level overrides the inherited
|
|
2644
|
+
// session thinking for THIS routed turn only; unset falls back to exactly today's
|
|
2645
|
+
// inherit-and-clamp behavior. Executor routes carry tier "cheap" too, so reasonCode is
|
|
2646
|
+
// checked first — otherwise an executor turn would silently pick up cheapThinking instead.
|
|
2647
|
+
// The judge's own completion has a separate knob (judgeThinking) applied at its call site.
|
|
2648
|
+
const routerThinkingSettings = this.settingsManager.getModelRouterSettings();
|
|
2649
|
+
const configuredThinking = !routeDecision
|
|
2650
|
+
? undefined
|
|
2651
|
+
: routeDecision.reasonCode === "executor_direct"
|
|
2652
|
+
? routerThinkingSettings.executorThinking
|
|
2653
|
+
: routeDecision.tier === "cheap"
|
|
2654
|
+
? routerThinkingSettings.cheapThinking
|
|
2655
|
+
: routeDecision.tier === "medium"
|
|
2656
|
+
? routerThinkingSettings.mediumThinking
|
|
2657
|
+
: routeDecision.tier === "expensive"
|
|
2658
|
+
? routerThinkingSettings.expensiveThinking
|
|
2659
|
+
: undefined;
|
|
2660
|
+
this.agent.state.thinkingLevel = clampThinkingLevel(routedModel, configuredThinking ?? previousThinkingLevel);
|
|
2405
2661
|
// G4: capability tool-filtering follows the ROUTED model for the turn. Without this a
|
|
2406
2662
|
// cheap/local routed model inherits the session model's full tool surface — schemas it
|
|
2407
2663
|
// pays for on every request and may not be able to drive at all.
|
|
@@ -2411,7 +2667,16 @@ export class AgentSession {
|
|
|
2411
2667
|
});
|
|
2412
2668
|
if (routedProfile.class !== "full") {
|
|
2413
2669
|
const allowed = new Set(filterToolNamesForCapability(previousTurnTools.map((tool) => tool.name), routedProfile));
|
|
2414
|
-
|
|
2670
|
+
swappedTools = previousTurnTools.filter((tool) => allowed.has(tool.name));
|
|
2671
|
+
this.agent.state.tools = swappedTools;
|
|
2672
|
+
// G4: the system prompt follows the ROUTED model's filtered surface too — otherwise the
|
|
2673
|
+
// cheap/local model is billed for (and told about) tool guidelines/snippets it can't call.
|
|
2674
|
+
// Per-turn only; restored in the finally. A live extension override of the prompt is left
|
|
2675
|
+
// alone (only shed when we're on the base prompt).
|
|
2676
|
+
if (this.agent.state.systemPrompt === this._baseSystemPrompt) {
|
|
2677
|
+
swappedSystemPrompt = this._buildSystemPromptForToolNames(this.agent.state.tools.map((tool) => tool.name));
|
|
2678
|
+
this.agent.state.systemPrompt = swappedSystemPrompt;
|
|
2679
|
+
}
|
|
2415
2680
|
}
|
|
2416
2681
|
}
|
|
2417
2682
|
try {
|
|
@@ -2445,6 +2710,16 @@ export class AgentSession {
|
|
|
2445
2710
|
};
|
|
2446
2711
|
this._lastModelRouterDecision = completedDecision;
|
|
2447
2712
|
}
|
|
2713
|
+
else {
|
|
2714
|
+
// The muscle missed AND the reflex brain could not refine the request into a toolkit
|
|
2715
|
+
// instruction (no fit brain model, or no confident plan). There is deliberately NO
|
|
2716
|
+
// frontier fallback here, so surface the miss instead of letting it stand silently —
|
|
2717
|
+
// otherwise the routed turn just ends with an unrun command and no explanation.
|
|
2718
|
+
this._emit({
|
|
2719
|
+
type: "warning",
|
|
2720
|
+
message: "Executor lane: the toolkit command did not run and the reflex brain could not refine it into an explicit instruction; leaving the turn as-is (no automatic escalation).",
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2448
2723
|
}
|
|
2449
2724
|
if (bufferRoutedTurn && this._modelRouterEscalationRequested) {
|
|
2450
2725
|
this.agent.state.messages.splice(originalHistoryLength);
|
|
@@ -2480,7 +2755,18 @@ export class AgentSession {
|
|
|
2480
2755
|
if (modelsAreEqual(this.agent.state.model, routedModel)) {
|
|
2481
2756
|
this.agent.state.model = previousModel;
|
|
2482
2757
|
this.agent.state.thinkingLevel = previousThinkingLevel;
|
|
2483
|
-
|
|
2758
|
+
// Symmetric restore (Bug G): undo tools/systemPrompt only if each is STILL the exact
|
|
2759
|
+
// reference/string the G4 swap above assigned (never assigned at all when the routed
|
|
2760
|
+
// profile was full-class — then there is nothing to restore either). An extension calling
|
|
2761
|
+
// setActiveToolsByName mid-turn reassigns both to its own values without touching the
|
|
2762
|
+
// model — the model guard above still passes, but that live change is legitimate and must
|
|
2763
|
+
// survive rather than being silently reverted to the stale pre-turn snapshot.
|
|
2764
|
+
if (swappedTools !== undefined && this.agent.state.tools === swappedTools) {
|
|
2765
|
+
this.agent.state.tools = previousTurnTools;
|
|
2766
|
+
}
|
|
2767
|
+
if (swappedSystemPrompt !== undefined && this.agent.state.systemPrompt === swappedSystemPrompt) {
|
|
2768
|
+
this.agent.state.systemPrompt = previousSystemPrompt;
|
|
2769
|
+
}
|
|
2484
2770
|
// The registry may have changed mid-turn (command-time registerProvider): re-resolve
|
|
2485
2771
|
// the restored model so a provider override is not dropped with the routed model.
|
|
2486
2772
|
this._refreshCurrentModelFromRegistry();
|
|
@@ -2605,6 +2891,10 @@ export class AgentSession {
|
|
|
2605
2891
|
let messages;
|
|
2606
2892
|
let routedTurnModel;
|
|
2607
2893
|
let routedTurnRouteDecision;
|
|
2894
|
+
// Built and painted early (see below) so a later throw in this try block — e.g. no model
|
|
2895
|
+
// selected/authenticated — can un-register it from _earlyDisplayedUserMessages instead of
|
|
2896
|
+
// leaking the reference forever.
|
|
2897
|
+
let userMessage;
|
|
2608
2898
|
// R4 effectiveness feedback: remember the recall page + the query so we can score, after the
|
|
2609
2899
|
// response, whether the agent actually used the recalled context.
|
|
2610
2900
|
let injectedRecall = "";
|
|
@@ -2667,13 +2957,38 @@ export class AgentSession {
|
|
|
2667
2957
|
}
|
|
2668
2958
|
// Flush any pending bash messages before the new prompt
|
|
2669
2959
|
this._flushPendingBashMessages();
|
|
2960
|
+
// Build the user message now — before the router judge — and paint it to the UI
|
|
2961
|
+
// immediately via a synthetic message_start. The judge is a real bounded LLM completion
|
|
2962
|
+
// (seconds), not a regex; awaiting it first made the prompt appear to hang. The
|
|
2963
|
+
// authoritative message_start emitted later for this SAME object is suppressed in
|
|
2964
|
+
// _handleAgentEvent (see _earlyDisplayedUserMessages) so it is still shown exactly once.
|
|
2965
|
+
const userContent = [{ type: "text", text: expandedText }];
|
|
2966
|
+
if (currentImages) {
|
|
2967
|
+
userContent.push(...currentImages);
|
|
2968
|
+
}
|
|
2969
|
+
userMessage = {
|
|
2970
|
+
role: "user",
|
|
2971
|
+
content: userContent,
|
|
2972
|
+
timestamp: Date.now(),
|
|
2973
|
+
};
|
|
2974
|
+
this._earlyDisplayedUserMessages.add(userMessage);
|
|
2975
|
+
this._emit({ type: "message_start", message: userMessage });
|
|
2976
|
+
// Bracket the routing/prep phase (judge, model/auth checks, compaction, ...) so the UI can
|
|
2977
|
+
// show general "working" feedback for it — otherwise the user stares at their own echoed
|
|
2978
|
+
// prompt with nothing happening for however long the judge takes. routing_end is emitted
|
|
2979
|
+
// exactly once below: either in the catch block (this phase failed) or right after the try
|
|
2980
|
+
// block (this phase succeeded, whether or not it produced a turn to run).
|
|
2981
|
+
this._emit({ type: "routing_start" });
|
|
2670
2982
|
const resolvedRouteInfo = await this._resolveModelRouterTurnRouteJudged(expandedText, {
|
|
2671
2983
|
// Internally generated turns (goal continuation, lane follow-ups) never consult the judge:
|
|
2672
2984
|
// the regex floor already classified them, and a 20-turn loop must not buy 20 judge calls.
|
|
2673
2985
|
skipJudge: options?.autoContinueGoal === false,
|
|
2674
2986
|
});
|
|
2675
|
-
|
|
2676
|
-
|
|
2987
|
+
// #27: a route landing on a local (ollama) model must not hard-fail the turn just because
|
|
2988
|
+
// the server isn't up yet — boot/reuse it here, or escalate to a non-local tier.
|
|
2989
|
+
const readyRouteInfo = await this._ensureRouteModelReady(resolvedRouteInfo);
|
|
2990
|
+
routedTurnModel = readyRouteInfo?.model;
|
|
2991
|
+
routedTurnRouteDecision = readyRouteInfo?.decision;
|
|
2677
2992
|
const requestModel = routedTurnModel ?? this.model;
|
|
2678
2993
|
// Validate model
|
|
2679
2994
|
if (!requestModel) {
|
|
@@ -2719,16 +3034,9 @@ export class AgentSession {
|
|
|
2719
3034
|
// recall must never break a turn
|
|
2720
3035
|
}
|
|
2721
3036
|
}
|
|
2722
|
-
// Add user message
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
userContent.push(...currentImages);
|
|
2726
|
-
}
|
|
2727
|
-
messages.push({
|
|
2728
|
-
role: "user",
|
|
2729
|
-
content: userContent,
|
|
2730
|
-
timestamp: Date.now(),
|
|
2731
|
-
});
|
|
3037
|
+
// Add user message (built earlier, before the router judge, so it could be painted
|
|
3038
|
+
// immediately — see the early message_start emit above).
|
|
3039
|
+
messages.push(userMessage);
|
|
2732
3040
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
2733
3041
|
for (const msg of this._pendingNextTurnMessages) {
|
|
2734
3042
|
messages.push(msg);
|
|
@@ -2759,9 +3067,22 @@ export class AgentSession {
|
|
|
2759
3067
|
}
|
|
2760
3068
|
}
|
|
2761
3069
|
catch (error) {
|
|
3070
|
+
// The turn never reached _runAgentPrompt, so the authoritative message_start that would
|
|
3071
|
+
// normally consume this entry (see _handleAgentEvent) never fires — un-register it here
|
|
3072
|
+
// instead of leaking the reference.
|
|
3073
|
+
if (userMessage) {
|
|
3074
|
+
this._earlyDisplayedUserMessages.delete(userMessage);
|
|
3075
|
+
}
|
|
3076
|
+
// The routing/prep phase (routing_start above) failed before ever reaching the turn — end
|
|
3077
|
+
// it here, or the UI's "working" indicator for it spins forever with nothing behind it.
|
|
3078
|
+
this._emit({ type: "routing_end" });
|
|
2762
3079
|
preflightResult?.(false);
|
|
2763
3080
|
throw error;
|
|
2764
3081
|
}
|
|
3082
|
+
// The routing/prep phase is over — either we're about to hand off into the turn (which emits
|
|
3083
|
+
// its own agent_start/streaming events right after), or messages is unexpectedly unset and we
|
|
3084
|
+
// bail below. Either way nothing is left "routing" past this point.
|
|
3085
|
+
this._emit({ type: "routing_end" });
|
|
2765
3086
|
if (!messages) {
|
|
2766
3087
|
return;
|
|
2767
3088
|
}
|
|
@@ -3939,15 +4260,22 @@ export class AgentSession {
|
|
|
3939
4260
|
}
|
|
3940
4261
|
_filterExtensionsForRuntime(extensions) {
|
|
3941
4262
|
this._inertExtensionWarnings = [];
|
|
4263
|
+
this._profileDeniedExtensionCount = 0;
|
|
3942
4264
|
if (this.settingsManager.getActiveResourceProfileNames().length === 0) {
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
4265
|
+
if (this.settingsManager.hasExplicitActiveResourceProfileSelection()) {
|
|
4266
|
+
// An explicit profile selection that resolves to no active profile is a deliberate
|
|
4267
|
+
// deny-all — every extension is withheld by that choice.
|
|
4268
|
+
this._profileDeniedExtensionCount = extensions.length;
|
|
4269
|
+
return [];
|
|
4270
|
+
}
|
|
4271
|
+
// No profile in play: only inline/SDK extensions load by default. That is the baseline, not
|
|
4272
|
+
// a profile denial, so it is not counted as withheld.
|
|
4273
|
+
return extensions.filter((extension) => extension.sourceInfo.source === "inline");
|
|
3946
4274
|
}
|
|
3947
4275
|
const hasToolOrCommandGate = this._hasToolOrCommandProfileGate();
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
4276
|
+
const allowedExtensions = extensions.filter((extension) => this.settingsManager.isResourceAllowedByProfile("extensions", extension.path, extension.sourceInfo.baseDir));
|
|
4277
|
+
this._profileDeniedExtensionCount = extensions.length - allowedExtensions.length;
|
|
4278
|
+
return allowedExtensions.map((extension) => {
|
|
3951
4279
|
if (!hasToolOrCommandGate)
|
|
3952
4280
|
return extension;
|
|
3953
4281
|
const tools = new Map(Array.from(extension.tools.entries()).filter(([name]) => this._isToolOrCommandAllowedByProfile(name)));
|
|
@@ -3962,6 +4290,36 @@ export class AgentSession {
|
|
|
3962
4290
|
return { ...extension, tools, commands };
|
|
3963
4291
|
});
|
|
3964
4292
|
}
|
|
4293
|
+
/**
|
|
4294
|
+
* /context observations for skills/prompts/extensions the active resource profile removed from
|
|
4295
|
+
* listings — the analog of the withheld-AGENTS.md warning. Strict UAC makes these silently absent,
|
|
4296
|
+
* so a lean profile's effect on the resource surface stays visible. Counts are profile-scoped
|
|
4297
|
+
* (skills/prompts via the profile-independent discovery universe filtered by the live profile
|
|
4298
|
+
* filter; extensions via the runtime filter's denied tally). Empty when nothing is withheld.
|
|
4299
|
+
*
|
|
4300
|
+
* Uses `isResourceDeniedByActiveProfile` (profile-only), not `isResourceAllowedByProfile` (which
|
|
4301
|
+
* also folds in the user's own legacy `disabledResources` list): a plain user-disabled resource
|
|
4302
|
+
* must never be misattributed to "the active resource profile" — that case is already surfaced by
|
|
4303
|
+
* the G14 disable-wins warning. With no active profile at all, the helper always reports nothing
|
|
4304
|
+
* denied, so this naturally stays silent (extensions keep their own runtime-filter-derived count,
|
|
4305
|
+
* which is already correctly zero absent a profile).
|
|
4306
|
+
*/
|
|
4307
|
+
_profileDeniedResourceObservations() {
|
|
4308
|
+
const observations = [];
|
|
4309
|
+
const withheld = (kind, paths) => paths.filter((path) => this.settingsManager.isResourceDeniedByActiveProfile(kind, path, this._cwd)).length;
|
|
4310
|
+
const skillsWithheld = withheld("skills", this._resourceLoader.getDiscoverableSkillPaths());
|
|
4311
|
+
if (skillsWithheld > 0) {
|
|
4312
|
+
observations.push(`${skillsWithheld} skill(s) withheld by the active resource profile — grant the "skills" kind to restore them`);
|
|
4313
|
+
}
|
|
4314
|
+
const promptsWithheld = withheld("prompts", this._resourceLoader.getDiscoverablePromptPaths());
|
|
4315
|
+
if (promptsWithheld > 0) {
|
|
4316
|
+
observations.push(`${promptsWithheld} prompt(s) withheld by the active resource profile — grant the "prompts" kind to restore them`);
|
|
4317
|
+
}
|
|
4318
|
+
if (this._profileDeniedExtensionCount > 0) {
|
|
4319
|
+
observations.push(`${this._profileDeniedExtensionCount} extension(s) withheld by the active resource profile — grant the "extensions" kind to restore them`);
|
|
4320
|
+
}
|
|
4321
|
+
return observations;
|
|
4322
|
+
}
|
|
3965
4323
|
/**
|
|
3966
4324
|
* Re-resolve the active resource profile's model/thinking from current settings and apply it.
|
|
3967
4325
|
* Only acts when the profile actually binds model/thinking AND that field was not set by an
|
|
@@ -5726,6 +6084,8 @@ export class AgentSession {
|
|
|
5726
6084
|
maxWallClockMs: settings.maxWallClockMs,
|
|
5727
6085
|
usageReportId,
|
|
5728
6086
|
signal: this._workerDelegationAbort.signal,
|
|
6087
|
+
// Parent validation must use the same relative-path baseline the runner reports in.
|
|
6088
|
+
cwd: this._cwd,
|
|
5729
6089
|
// Write lane (G2): runner-side action application through the envelope path scope.
|
|
5730
6090
|
applyActions: workerRequest.envelope.capabilities.includes("write_files")
|
|
5731
6091
|
? (actions) => applyWorkerActions({ actions, envelope: workerRequest.envelope, cwd: this._cwd })
|
|
@@ -6104,7 +6464,10 @@ export class AgentSession {
|
|
|
6104
6464
|
proposal,
|
|
6105
6465
|
confidence: policy.reflectionSourceConfidence,
|
|
6106
6466
|
observations,
|
|
6107
|
-
|
|
6467
|
+
// A replace/remove supersedes an existing durable fact — the reflection engine's
|
|
6468
|
+
// confront-before-write conflict signal — so it routes through approval instead of
|
|
6469
|
+
// silently overwriting prior memory. Additive writes contradict nothing.
|
|
6470
|
+
contradictions: contradictionsForReflectionWrite(write),
|
|
6108
6471
|
settings: {
|
|
6109
6472
|
enabled: true,
|
|
6110
6473
|
autoApplyEnabled: policy.autoApplyEnabled,
|
|
@@ -6112,6 +6475,7 @@ export class AgentSession {
|
|
|
6112
6475
|
minObservations: policy.minObservations,
|
|
6113
6476
|
allowedAutoApplyLayers: policy.allowedAutoApplyLayers,
|
|
6114
6477
|
requireRollbackPlan: policy.requireRollbackPlan,
|
|
6478
|
+
autoApplySupersessions: policy.autoApplySupersessions,
|
|
6115
6479
|
},
|
|
6116
6480
|
})
|
|
6117
6481
|
: {
|
|
@@ -6147,20 +6511,25 @@ export class AgentSession {
|
|
|
6147
6511
|
},
|
|
6148
6512
|
});
|
|
6149
6513
|
}
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6514
|
+
// The gate's decision and the write's actual outcome are two different questions: the memory
|
|
6515
|
+
// tool can refuse a write (budget exceeded, drift, threat) via details.success:false without
|
|
6516
|
+
// throwing. Capture that outcome instead of assuming "decision.kind === apply" means it landed
|
|
6517
|
+
// — otherwise a refused write leaves a phantom "apply" audit whose rollback later fails
|
|
6518
|
+
// not-found (or, worse, misfires against whatever now occupies that text).
|
|
6519
|
+
const applied = decision.kind === "apply" ? await this._applyReflectionWrite(write, signal) : false;
|
|
6520
|
+
const writeFailed = decision.kind === "apply" && !applied;
|
|
6153
6521
|
if (decision.kind !== "no-op") {
|
|
6154
6522
|
auditSequence += 1;
|
|
6155
6523
|
appendLearningAuditSnapshot(this.sessionManager, {
|
|
6156
6524
|
id: `audit-${auditSequence}`,
|
|
6157
6525
|
proposalId,
|
|
6158
6526
|
layer: proposal.layer,
|
|
6159
|
-
action: decision.kind === "apply" ? "apply" : "propose",
|
|
6527
|
+
action: writeFailed ? "apply_failed" : decision.kind === "apply" ? "apply" : "propose",
|
|
6160
6528
|
summary: proposal.summary,
|
|
6161
|
-
reasonCode: decision.reasonCode,
|
|
6529
|
+
reasonCode: writeFailed ? APPLY_WRITE_REFUSED_REASON_CODE : decision.reasonCode,
|
|
6162
6530
|
decision,
|
|
6163
|
-
rollback,
|
|
6531
|
+
// No rollback plan on a failed apply — nothing durable landed, so there is nothing to undo.
|
|
6532
|
+
rollback: writeFailed ? undefined : rollback,
|
|
6164
6533
|
createdAt: new Date().toISOString(),
|
|
6165
6534
|
});
|
|
6166
6535
|
}
|
|
@@ -6196,28 +6565,41 @@ export class AgentSession {
|
|
|
6196
6565
|
const rollback = audit.rollback;
|
|
6197
6566
|
if (!rollback)
|
|
6198
6567
|
return { ok: false, reason: "no_rollback_plan" };
|
|
6568
|
+
// Every inverse must be VERIFIED-applied before the rollback audit is appended: a silently
|
|
6569
|
+
// failed inverse that still recorded "rollback" would permanently self-lock the change
|
|
6570
|
+
// behind already_rolled_back while the durable write is in fact still live.
|
|
6199
6571
|
switch (rollback.kind) {
|
|
6200
6572
|
case "memory_remove": {
|
|
6201
6573
|
if (!rollback.target)
|
|
6202
6574
|
return { ok: false, reason: "missing_rollback_target" };
|
|
6203
|
-
await this._applyReflectionWrite({ kind: "memory_remove", target: rollback.target })
|
|
6575
|
+
if (!(await this._applyReflectionWrite({ kind: "memory_remove", target: rollback.target }))) {
|
|
6576
|
+
return { ok: false, reason: "rollback_apply_failed" };
|
|
6577
|
+
}
|
|
6204
6578
|
break;
|
|
6205
6579
|
}
|
|
6206
6580
|
case "memory_restore": {
|
|
6207
6581
|
if (!rollback.target || rollback.previous === undefined) {
|
|
6208
6582
|
return { ok: false, reason: "missing_rollback_target" };
|
|
6209
6583
|
}
|
|
6210
|
-
await this._applyReflectionWrite({
|
|
6584
|
+
const applied = await this._applyReflectionWrite({
|
|
6211
6585
|
kind: "memory_replace",
|
|
6212
6586
|
target: rollback.target,
|
|
6213
6587
|
text: rollback.previous,
|
|
6214
6588
|
});
|
|
6589
|
+
if (!applied)
|
|
6590
|
+
return { ok: false, reason: "rollback_apply_failed" };
|
|
6215
6591
|
break;
|
|
6216
6592
|
}
|
|
6217
6593
|
case "memory_add": {
|
|
6218
6594
|
if (rollback.previous === undefined)
|
|
6219
6595
|
return { ok: false, reason: "missing_rollback_target" };
|
|
6220
|
-
await this._applyReflectionWrite({
|
|
6596
|
+
const applied = await this._applyReflectionWrite({
|
|
6597
|
+
kind: "memory_add",
|
|
6598
|
+
section: "MEMORY",
|
|
6599
|
+
text: rollback.previous,
|
|
6600
|
+
});
|
|
6601
|
+
if (!applied)
|
|
6602
|
+
return { ok: false, reason: "rollback_apply_failed" };
|
|
6221
6603
|
break;
|
|
6222
6604
|
}
|
|
6223
6605
|
case "archive_skill": {
|
|
@@ -6245,28 +6627,34 @@ export class AgentSession {
|
|
|
6245
6627
|
/**
|
|
6246
6628
|
* Apply one reflection write through the bundled `memory` tool. `memory_replace`/`memory_remove`
|
|
6247
6629
|
* don't carry a target file, so we try MEMORY.md first and fall back to USER.md when the substring
|
|
6248
|
-
* isn't found there.
|
|
6630
|
+
* isn't found there. Never throws (reflection must never break a turn); returns whether the write
|
|
6631
|
+
* actually applied so callers that MUST know — rollback's once-only accounting — can react instead
|
|
6632
|
+
* of recording a success that never happened.
|
|
6249
6633
|
*/
|
|
6250
6634
|
async _applyReflectionWrite(write, signal) {
|
|
6251
6635
|
// R7 memory-to-behavior: a recurring procedure is compiled into an executable skill file rather
|
|
6252
6636
|
// than stored as a flat fact. Written under the agent skills dir so it loads like any user skill.
|
|
6253
6637
|
if (write.kind === "promote_skill") {
|
|
6254
|
-
this._promoteReflectionSkill(write.name, write.description, write.body);
|
|
6255
|
-
return;
|
|
6638
|
+
return this._promoteReflectionSkill(write.name, write.description, write.body);
|
|
6256
6639
|
}
|
|
6257
6640
|
const memTool = this._memoryManager.getToolDefinitions().find((t) => t.name === "memory");
|
|
6258
6641
|
const exec = memTool?.execute;
|
|
6259
6642
|
if (!exec)
|
|
6260
|
-
return;
|
|
6643
|
+
return false;
|
|
6261
6644
|
const run = (params) => exec("reflection", params, signal, undefined, undefined);
|
|
6262
6645
|
if (write.kind === "memory_add") {
|
|
6263
6646
|
try {
|
|
6264
|
-
await run({
|
|
6647
|
+
const res = await run({
|
|
6648
|
+
action: "add",
|
|
6649
|
+
target: write.section === "USER" ? "user" : "memory",
|
|
6650
|
+
content: write.text,
|
|
6651
|
+
});
|
|
6652
|
+
return res?.details?.success === true;
|
|
6265
6653
|
}
|
|
6266
6654
|
catch {
|
|
6267
6655
|
// best-effort; reflection writes must never throw into the turn loop
|
|
6656
|
+
return false;
|
|
6268
6657
|
}
|
|
6269
|
-
return;
|
|
6270
6658
|
}
|
|
6271
6659
|
// replace / remove carry no target file — try MEMORY.md, then USER.md. The memory tool reports
|
|
6272
6660
|
// outcomes via `details.success` (it catches its own errors rather than throwing). Only a
|
|
@@ -6279,15 +6667,16 @@ export class AgentSession {
|
|
|
6279
6667
|
: { action: "remove", target, oldContent: write.target };
|
|
6280
6668
|
const res = await run(params);
|
|
6281
6669
|
if (res?.details?.success === true)
|
|
6282
|
-
return; // applied
|
|
6670
|
+
return true; // applied
|
|
6283
6671
|
if (!/not found/i.test(String(res?.details?.error ?? "")))
|
|
6284
|
-
return; // real failure — don't misapply
|
|
6672
|
+
return false; // real failure — don't misapply
|
|
6285
6673
|
// substring simply absent from this file — try the next target
|
|
6286
6674
|
}
|
|
6287
6675
|
catch {
|
|
6288
6676
|
// defensive: if the tool ever does throw, try the next target
|
|
6289
6677
|
}
|
|
6290
6678
|
}
|
|
6679
|
+
return false;
|
|
6291
6680
|
}
|
|
6292
6681
|
/**
|
|
6293
6682
|
* R7: write a reflection-promoted skill as `<agentDir>/skills/<name>/SKILL.md` so it loads like any
|
|
@@ -6301,21 +6690,23 @@ export class AgentSession {
|
|
|
6301
6690
|
.replace(/^-+|-+$/g, "")
|
|
6302
6691
|
.slice(0, 64);
|
|
6303
6692
|
if (!name || !body.trim())
|
|
6304
|
-
return;
|
|
6693
|
+
return false;
|
|
6305
6694
|
try {
|
|
6306
6695
|
const dir = join(this._agentDir, "skills", name);
|
|
6307
6696
|
const file = join(dir, "SKILL.md");
|
|
6308
6697
|
if (existsSync(file))
|
|
6309
|
-
return; // do not overwrite an existing skill
|
|
6698
|
+
return false; // do not overwrite an existing skill
|
|
6310
6699
|
mkdirSync(dir, { recursive: true });
|
|
6311
6700
|
const safeDescription = description.replace(/[\r\n]+/g, " ").trim();
|
|
6312
6701
|
// `promoted: true` marks this as reflection-generated so the curator (#32) can lifecycle-manage
|
|
6313
6702
|
// it (archive/consolidate) WITHOUT ever touching hand-authored user skills.
|
|
6314
6703
|
const content = `---\nname: ${name}\ndescription: ${safeDescription}\npromoted: true\n---\n\n<!-- Auto-generated by the reflection engine (R7 memory-to-behavior). Review and refine. -->\n\n${body.trim()}\n`;
|
|
6315
6704
|
writeFileSync(file, content, "utf-8");
|
|
6705
|
+
return true;
|
|
6316
6706
|
}
|
|
6317
6707
|
catch {
|
|
6318
6708
|
// promotion must never break a turn
|
|
6709
|
+
return false;
|
|
6319
6710
|
}
|
|
6320
6711
|
}
|
|
6321
6712
|
getContextUsage() {
|