@ouro.bot/cli 0.1.0-alpha.483 → 0.1.0-alpha.484
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.json +6 -0
- package/dist/heart/provider-failover.js +35 -0
- package/dist/senses/pipeline.js +68 -6
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.484",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Agent-driven failover (the `switch to <provider>` reply path) now re-pings the candidate provider before mutating provider state. If credentials are missing or the ping fails, the active lane is left untouched, a `senses.failover_switch_refused` event is emitted, and the agent receives an operational refusal context message naming the lane it is still standing on plus the verified alternatives that remain so the next turn does not re-enter discovery mode."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.483",
|
|
6
12
|
"changes": [
|
|
@@ -5,6 +5,7 @@ exports.formatReadyProviderLabel = formatReadyProviderLabel;
|
|
|
5
5
|
exports.buildFailoverContext = buildFailoverContext;
|
|
6
6
|
exports.handleFailoverReply = handleFailoverReply;
|
|
7
7
|
exports.runMachineProviderFailoverInventory = runMachineProviderFailoverInventory;
|
|
8
|
+
exports.validateFailoverSwitchCandidate = validateFailoverSwitchCandidate;
|
|
8
9
|
const identity_1 = require("./identity");
|
|
9
10
|
const provider_ping_1 = require("./provider-ping");
|
|
10
11
|
const provider_models_1 = require("./provider-models");
|
|
@@ -170,6 +171,7 @@ function buildFailoverContext(errorMessage, classification, currentProvider, cur
|
|
|
170
171
|
errorSummary,
|
|
171
172
|
classification,
|
|
172
173
|
currentProvider,
|
|
174
|
+
currentModel,
|
|
173
175
|
currentLane,
|
|
174
176
|
agentName,
|
|
175
177
|
workingProviders,
|
|
@@ -264,3 +266,36 @@ async function runMachineProviderFailoverInventory(agentName, currentProvider, o
|
|
|
264
266
|
});
|
|
265
267
|
return inventory;
|
|
266
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Re-verify a failover candidate is actually reachable right before we mutate
|
|
271
|
+
* provider state. The inventory ping that produced the candidate may be stale
|
|
272
|
+
* (creds revoked between inventory and reply); without this preflight, an
|
|
273
|
+
* agent-driven "switch to <provider>" can move the lane onto an unreachable
|
|
274
|
+
* provider and brick the next turn.
|
|
275
|
+
*/
|
|
276
|
+
async function validateFailoverSwitchCandidate(agentName, candidate, options = {}) {
|
|
277
|
+
const ping = options.ping ?? provider_ping_1.pingProvider;
|
|
278
|
+
const refreshPool = options.refreshPool ?? provider_credentials_1.refreshProviderCredentialPool;
|
|
279
|
+
const poolResult = await refreshPool(agentName);
|
|
280
|
+
if (!poolResult.ok) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
classification: "auth-failure",
|
|
284
|
+
message: `provider credential pool unavailable (${poolResult.reason}): ${poolResult.error}`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const record = poolResult.pool.providers[candidate.provider];
|
|
288
|
+
if (!record) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
classification: "auth-failure",
|
|
292
|
+
message: `no credentials configured for ${candidate.provider}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const config = { ...record.credentials, ...record.config };
|
|
296
|
+
const result = await ping(candidate.provider, config, { model: candidate.model });
|
|
297
|
+
if (!result.ok) {
|
|
298
|
+
return { ok: false, classification: result.classification, message: result.message };
|
|
299
|
+
}
|
|
300
|
+
return { ok: true };
|
|
301
|
+
}
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -97,7 +97,23 @@ function resolveCurrentFailoverBinding(agentName, lane) {
|
|
|
97
97
|
const fallback = lane === "inner" ? agentConfig.agentFacing : agentConfig.humanFacing;
|
|
98
98
|
return { provider: fallback.provider, model: fallback.model };
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Apply an agent-driven failover switch to provider state, but only after
|
|
102
|
+
* re-pinging the candidate. The inventory ping that produced the candidate
|
|
103
|
+
* may be stale by the time the agent replies — without this preflight, a
|
|
104
|
+
* "switch to <provider>" reply can move the lane onto an unreachable provider.
|
|
105
|
+
*
|
|
106
|
+
* Returns:
|
|
107
|
+
* { ok: true } — preflight passed, state mutated
|
|
108
|
+
* { ok: false, refused } — preflight failed, state untouched, caller should
|
|
109
|
+
* surface the refusal to the agent
|
|
110
|
+
* Throws on disk errors only (caught by caller as before).
|
|
111
|
+
*/
|
|
112
|
+
async function writeFailoverProviderStateSwitch(agentName, action) {
|
|
113
|
+
const validation = await (0, provider_failover_1.validateFailoverSwitchCandidate)(agentName, { provider: action.provider, model: action.model });
|
|
114
|
+
if (!validation.ok) {
|
|
115
|
+
return { ok: false, refused: true, classification: validation.classification, message: validation.message };
|
|
116
|
+
}
|
|
101
117
|
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
102
118
|
const stateResult = (0, provider_state_1.readProviderState)(agentRoot);
|
|
103
119
|
if (!stateResult.ok) {
|
|
@@ -125,11 +141,36 @@ function writeFailoverProviderStateSwitch(agentName, action) {
|
|
|
125
141
|
lanes,
|
|
126
142
|
readiness,
|
|
127
143
|
});
|
|
144
|
+
return { ok: true };
|
|
128
145
|
}
|
|
129
146
|
function formatFailoverSwitchLabel(action) {
|
|
130
147
|
const provenance = (0, provider_failover_1.formatCredentialProvenanceLabel)(action);
|
|
131
148
|
return `${action.provider} (${action.model}${provenance ? `; ${provenance}` : ""})`;
|
|
132
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Build the operational refusal context message handed back to the agent when
|
|
152
|
+
* a failover switch is rejected by the preflight ping. Slugger-tested format:
|
|
153
|
+
* lead with the refusal + reason, restate the lane that's still standing, then
|
|
154
|
+
* list remaining ready alternatives so the next turn doesn't have to re-enter
|
|
155
|
+
* discovery mode.
|
|
156
|
+
*/
|
|
157
|
+
function buildFailoverSwitchRefusedMessage(pendingContext, refusedAction, refusal) {
|
|
158
|
+
const refusedLabel = formatFailoverSwitchLabel(refusedAction);
|
|
159
|
+
const remaining = pendingContext.readyProviders.filter((candidate) => candidate.provider !== refusedAction.provider);
|
|
160
|
+
const alternativesLine = remaining.length > 0
|
|
161
|
+
? `available verified alternatives right now: ${remaining.map((c) => `${c.provider} (${c.model})`).join(", ")}.`
|
|
162
|
+
: "no other verified alternatives are ready right now.";
|
|
163
|
+
const nextMove = remaining.length > 0
|
|
164
|
+
? `next move: reply "switch to <provider>" picking one of the alternatives above, or tell the user you cannot continue and why.`
|
|
165
|
+
: `next move: ask the operator to repair credentials for ${refusedAction.provider} (or another provider), or tell the user you cannot continue and why.`;
|
|
166
|
+
return [
|
|
167
|
+
`[provider switch refused: tried to switch ${refusedAction.lane} lane to ${refusedLabel}.`,
|
|
168
|
+
`reason: preflight ping failed (${refusal.classification}: ${refusal.message}).`,
|
|
169
|
+
`current lane unchanged: ${pendingContext.currentProvider} / ${pendingContext.currentModel} on the ${pendingContext.currentLane} lane.`,
|
|
170
|
+
alternativesLine,
|
|
171
|
+
nextMove + "]",
|
|
172
|
+
].join(" ");
|
|
173
|
+
}
|
|
133
174
|
function prependTurnSections(message, sections) {
|
|
134
175
|
/* v8 ignore next -- defensive: only user messages with non-empty sections reach here @preserve */
|
|
135
176
|
if (message.role !== "user" || sections.length === 0)
|
|
@@ -176,10 +217,9 @@ async function handleInboundTurn(input) {
|
|
|
176
217
|
const failoverAgentName = pendingContext.agentName;
|
|
177
218
|
input.failoverState.pending = null; // always clear before acting
|
|
178
219
|
if (failoverAction.action === "switch") {
|
|
179
|
-
let
|
|
220
|
+
let switchOutcome = null;
|
|
180
221
|
try {
|
|
181
|
-
writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
|
|
182
|
-
switchSucceeded = true;
|
|
222
|
+
switchOutcome = await writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
|
|
183
223
|
/* v8 ignore start -- defensive: write failure during provider switch @preserve */
|
|
184
224
|
}
|
|
185
225
|
catch (switchError) {
|
|
@@ -192,8 +232,7 @@ async function handleInboundTurn(input) {
|
|
|
192
232
|
});
|
|
193
233
|
}
|
|
194
234
|
/* v8 ignore stop */
|
|
195
|
-
|
|
196
|
-
if (switchSucceeded) {
|
|
235
|
+
if (switchOutcome?.ok) {
|
|
197
236
|
(0, runtime_1.emitNervesEvent)({
|
|
198
237
|
component: "senses",
|
|
199
238
|
event: "senses.failover_switch",
|
|
@@ -217,6 +256,29 @@ async function handleInboundTurn(input) {
|
|
|
217
256
|
}];
|
|
218
257
|
input.switchedProvider = failoverAction.provider;
|
|
219
258
|
}
|
|
259
|
+
else if (switchOutcome && !switchOutcome.ok) {
|
|
260
|
+
// Preflight refused the switch — the candidate provider is not actually
|
|
261
|
+
// reachable right now. Keep the existing lane intact and tell the agent
|
|
262
|
+
// what happened so it can pick something else next turn.
|
|
263
|
+
(0, runtime_1.emitNervesEvent)({
|
|
264
|
+
level: "warn",
|
|
265
|
+
component: "senses",
|
|
266
|
+
event: "senses.failover_switch_refused",
|
|
267
|
+
message: `refused failover switch of ${failoverAction.lane} lane to ${failoverAction.provider}: ${switchOutcome.message}`,
|
|
268
|
+
meta: {
|
|
269
|
+
agentName: failoverAgentName,
|
|
270
|
+
lane: failoverAction.lane,
|
|
271
|
+
provider: failoverAction.provider,
|
|
272
|
+
model: failoverAction.model,
|
|
273
|
+
classification: switchOutcome.classification,
|
|
274
|
+
error: switchOutcome.message,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
input.messages = [{
|
|
278
|
+
role: "user",
|
|
279
|
+
content: buildFailoverSwitchRefusedMessage(pendingContext, failoverAction, switchOutcome),
|
|
280
|
+
}];
|
|
281
|
+
}
|
|
220
282
|
// Switch failed OR succeeded — either way, fall through to normal processing.
|
|
221
283
|
}
|
|
222
284
|
}
|