@ouro.bot/cli 0.1.0-alpha.326 → 0.1.0-alpha.328
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 +13 -0
- package/dist/heart/auth/auth-flow.js +14 -7
- package/dist/heart/core.js +29 -3
- package/dist/heart/daemon/cli-defaults.js +2 -7
- package/dist/heart/daemon/cli-exec.js +14 -1
- package/dist/heart/daemon/startup-tui.js +44 -34
- package/dist/heart/hatch/hatch-flow.js +4 -2
- package/dist/heart/provider-failover.js +70 -23
- package/dist/heart/provider-models.js +81 -0
- package/dist/senses/pipeline.js +5 -3
- package/package.json +2 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
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.328",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(auth): keep provider switches model-aware so changing providers updates incompatible model pairings to safe provider defaults instead of carrying stale model names across OpenAI Codex, Anthropic, MiniMax, Azure, and GitHub Copilot.",
|
|
8
|
+
"fix(auth): rewrite provider auth/failover guidance into multiline actionable sections with provider error details, model-mismatch repair commands, and ready-provider switch prompts."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.327",
|
|
13
|
+
"changes": [
|
|
14
|
+
"fix(daemon): make startup stability polling respect stdout TTY capability so captured `ouro up` output is plain append-only text without raw ANSI cursor-control or color escapes, while interactive terminals keep in-place progress rendering."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.326",
|
|
6
19
|
"changes": [
|
|
@@ -48,33 +48,29 @@ const path = __importStar(require("path"));
|
|
|
48
48
|
const runtime_1 = require("../../nerves/runtime");
|
|
49
49
|
const identity_1 = require("../identity");
|
|
50
50
|
const migrate_config_1 = require("../migrate-config");
|
|
51
|
+
const provider_models_1 = require("../provider-models");
|
|
51
52
|
const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
|
|
52
53
|
const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
|
|
53
54
|
const DEFAULT_SECRETS_TEMPLATE = {
|
|
54
55
|
providers: {
|
|
55
56
|
azure: {
|
|
56
|
-
modelName: "gpt-4o-mini",
|
|
57
57
|
apiKey: "",
|
|
58
58
|
endpoint: "",
|
|
59
59
|
deployment: "",
|
|
60
60
|
apiVersion: "2025-04-01-preview",
|
|
61
61
|
},
|
|
62
62
|
minimax: {
|
|
63
|
-
model: "MiniMax-M2.7",
|
|
64
63
|
apiKey: "",
|
|
65
64
|
},
|
|
66
65
|
anthropic: {
|
|
67
|
-
model: "claude-opus-4-6",
|
|
68
66
|
setupToken: "",
|
|
69
67
|
refreshToken: "",
|
|
70
68
|
expiresAt: 0,
|
|
71
69
|
},
|
|
72
70
|
"openai-codex": {
|
|
73
|
-
model: "gpt-5.4",
|
|
74
71
|
oauthAccessToken: "",
|
|
75
72
|
},
|
|
76
73
|
"github-copilot": {
|
|
77
|
-
model: "claude-sonnet-4.6",
|
|
78
74
|
githubToken: "",
|
|
79
75
|
baseUrl: "",
|
|
80
76
|
},
|
|
@@ -176,16 +172,27 @@ function readAgentConfigForAgent(agentName, bundlesRoot = (0, identity_1.getAgen
|
|
|
176
172
|
function writeAgentProviderSelection(agentName, facing, provider, bundlesRoot = (0, identity_1.getAgentBundlesRoot)()) {
|
|
177
173
|
const { configPath, config } = readAgentConfigForAgent(agentName, bundlesRoot);
|
|
178
174
|
const facingKey = facing === "human" ? "humanFacing" : "agentFacing";
|
|
175
|
+
const previousFacing = config[facingKey];
|
|
176
|
+
const resolved = (0, provider_models_1.resolveModelForProviderSelection)(provider, previousFacing.model);
|
|
179
177
|
const nextConfig = {
|
|
180
178
|
...config,
|
|
181
|
-
[facingKey]: { ...
|
|
179
|
+
[facingKey]: { ...previousFacing, provider, model: resolved.model },
|
|
182
180
|
};
|
|
183
181
|
fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
184
182
|
(0, runtime_1.emitNervesEvent)({
|
|
185
183
|
component: "daemon",
|
|
186
184
|
event: "daemon.auth_provider_selected",
|
|
187
185
|
message: "updated agent provider selection after auth flow",
|
|
188
|
-
meta: {
|
|
186
|
+
meta: {
|
|
187
|
+
agentName,
|
|
188
|
+
facing,
|
|
189
|
+
provider,
|
|
190
|
+
previousProvider: previousFacing.provider,
|
|
191
|
+
previousModel: previousFacing.model,
|
|
192
|
+
model: resolved.model,
|
|
193
|
+
preservedModel: resolved.preserved,
|
|
194
|
+
configPath,
|
|
195
|
+
},
|
|
189
196
|
});
|
|
190
197
|
return configPath;
|
|
191
198
|
}
|
package/dist/heart/core.js
CHANGED
|
@@ -32,6 +32,7 @@ const obligations_1 = require("../arc/obligations");
|
|
|
32
32
|
const tool_loop_1 = require("./tool-loop");
|
|
33
33
|
const packets_1 = require("../arc/packets");
|
|
34
34
|
const tool_friction_1 = require("./tool-friction");
|
|
35
|
+
const provider_models_1 = require("./provider-models");
|
|
35
36
|
const _providerRuntimes = {
|
|
36
37
|
human: null,
|
|
37
38
|
agent: null,
|
|
@@ -405,6 +406,33 @@ const RETRY_LABELS = {
|
|
|
405
406
|
"network-error": "network error",
|
|
406
407
|
"unknown": "error",
|
|
407
408
|
};
|
|
409
|
+
function buildAuthFailureGuidance(provider, model, agentName, detail) {
|
|
410
|
+
const mismatch = (0, provider_models_1.getProviderModelMismatchMessage)(provider, model);
|
|
411
|
+
const modelLabel = model
|
|
412
|
+
? mismatch
|
|
413
|
+
? `${provider} [configured model: ${model}]`
|
|
414
|
+
: `${provider} (${model})`
|
|
415
|
+
: provider;
|
|
416
|
+
const lines = [`${modelLabel} authentication failed.`];
|
|
417
|
+
const cleanDetail = detail.replace(/\s+/g, " ").trim();
|
|
418
|
+
if (cleanDetail)
|
|
419
|
+
lines.push(`provider detail: ${cleanDetail.length > 300 ? `${cleanDetail.slice(0, 297)}...` : cleanDetail}`);
|
|
420
|
+
lines.push("");
|
|
421
|
+
lines.push("To keep using this provider:");
|
|
422
|
+
lines.push(` 1. Run \`ouro auth --agent ${agentName} --provider ${provider}\``);
|
|
423
|
+
if (mismatch) {
|
|
424
|
+
const defaultModel = (0, provider_models_1.getDefaultModelForProvider)(provider);
|
|
425
|
+
lines.push("");
|
|
426
|
+
lines.push("Config warning:");
|
|
427
|
+
lines.push(` - ${mismatch}`);
|
|
428
|
+
lines.push(" - Repair the configured model with:");
|
|
429
|
+
lines.push(` \`ouro config model --agent ${agentName} --facing human ${defaultModel}\``);
|
|
430
|
+
lines.push(` \`ouro config model --agent ${agentName} --facing agent ${defaultModel}\``);
|
|
431
|
+
}
|
|
432
|
+
lines.push("");
|
|
433
|
+
lines.push(`To use another configured provider instead, run \`ouro auth switch --agent ${agentName} --provider <provider>\`.`);
|
|
434
|
+
return lines.join("\n");
|
|
435
|
+
}
|
|
408
436
|
async function runAgent(messages, callbacks, channel, signal, options) {
|
|
409
437
|
const facing = (0, channel_1.channelToFacing)(channel);
|
|
410
438
|
const providerRuntime = getProviderRuntime(facing);
|
|
@@ -1025,9 +1053,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
1025
1053
|
if (terminalErrorClassification === "auth-failure") {
|
|
1026
1054
|
const agentName = (0, identity_2.getAgentName)();
|
|
1027
1055
|
const currentProvider = providerRuntime.id;
|
|
1028
|
-
callbacks.onError(new Error(
|
|
1029
|
-
`Run \`ouro auth --agent ${agentName} --provider ${currentProvider}\` to refresh credentials, ` +
|
|
1030
|
-
`or \`ouro auth switch --agent ${agentName} --provider <other>\` to switch providers.`), "terminal");
|
|
1056
|
+
callbacks.onError(new Error(buildAuthFailureGuidance(currentProvider, providerRuntime.model, agentName, terminalError.message)), "terminal");
|
|
1031
1057
|
}
|
|
1032
1058
|
else {
|
|
1033
1059
|
callbacks.onError(terminalError, "terminal");
|
|
@@ -70,6 +70,7 @@ const launchd_1 = require("./launchd");
|
|
|
70
70
|
const socket_client_1 = require("./socket-client");
|
|
71
71
|
const session_activity_1 = require("../session-activity");
|
|
72
72
|
const auth_flow_1 = require("../auth/auth-flow");
|
|
73
|
+
const provider_models_1 = require("../provider-models");
|
|
73
74
|
const cli_parse_1 = require("./cli-parse");
|
|
74
75
|
const provider_discovery_1 = require("./provider-discovery");
|
|
75
76
|
// ── Default implementations ──
|
|
@@ -305,13 +306,7 @@ async function defaultRunSerpentGuide() {
|
|
|
305
306
|
const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
|
|
306
307
|
const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
|
|
307
308
|
// Default models per provider (used when entering new credentials)
|
|
308
|
-
const defaultModels =
|
|
309
|
-
anthropic: "claude-opus-4-6",
|
|
310
|
-
minimax: "MiniMax-M2.7",
|
|
311
|
-
"openai-codex": "gpt-5.4",
|
|
312
|
-
"github-copilot": "claude-sonnet-4.6",
|
|
313
|
-
azure: "",
|
|
314
|
-
};
|
|
309
|
+
const defaultModels = provider_models_1.DEFAULT_PROVIDER_MODELS;
|
|
315
310
|
// Scan environment variables for API keys using the shared helper
|
|
316
311
|
const envCreds = (0, provider_discovery_1.scanEnvVarCredentials)(process.env);
|
|
317
312
|
const envDiscovered = [];
|
|
@@ -167,6 +167,8 @@ async function ensureDaemonRunning(deps) {
|
|
|
167
167
|
daemonPid: runtimeResult.startedPid ?? null,
|
|
168
168
|
/* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
|
|
169
169
|
writeRaw: (text) => process.stdout.write(text),
|
|
170
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected for captured-output safety @preserve */
|
|
171
|
+
isTTY: process.stdout.isTTY === true,
|
|
170
172
|
/* v8 ignore next -- thin wrapper: real Date.now() injected for testability @preserve */
|
|
171
173
|
now: () => Date.now(),
|
|
172
174
|
/* v8 ignore next -- thin wrapper: real setTimeout injected for testability @preserve */
|
|
@@ -205,6 +207,8 @@ async function ensureDaemonRunning(deps) {
|
|
|
205
207
|
daemonPid: lastPid,
|
|
206
208
|
/* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
|
|
207
209
|
writeRaw: (text) => process.stdout.write(text),
|
|
210
|
+
/* v8 ignore next -- thin wrapper: real stdout TTY detection injected for captured-output safety @preserve */
|
|
211
|
+
isTTY: process.stdout.isTTY === true,
|
|
208
212
|
/* v8 ignore next -- thin wrapper: real Date.now() injected for testability @preserve */
|
|
209
213
|
now: () => Date.now(),
|
|
210
214
|
/* v8 ignore next -- thin wrapper: real setTimeout injected for testability @preserve */
|
|
@@ -1772,7 +1776,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
1772
1776
|
(0, auth_flow_1.writeAgentProviderSelection)(command.agent, "human", command.provider, deps.bundlesRoot);
|
|
1773
1777
|
(0, auth_flow_1.writeAgentProviderSelection)(command.agent, "agent", command.provider, deps.bundlesRoot);
|
|
1774
1778
|
}
|
|
1775
|
-
const
|
|
1779
|
+
const { config: updatedConfig } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
|
|
1780
|
+
const facingSummary = command.facing
|
|
1781
|
+
? (() => {
|
|
1782
|
+
const facingConfig = command.facing === "human" ? updatedConfig.humanFacing : updatedConfig.agentFacing;
|
|
1783
|
+
return `${command.facing} model: ${facingConfig.model}`;
|
|
1784
|
+
})()
|
|
1785
|
+
: updatedConfig.humanFacing.model === updatedConfig.agentFacing.model
|
|
1786
|
+
? `model: ${updatedConfig.humanFacing.model}`
|
|
1787
|
+
: `human model: ${updatedConfig.humanFacing.model}; agent model: ${updatedConfig.agentFacing.model}`;
|
|
1788
|
+
const message = `switched ${command.agent} to ${command.provider} (${facingSummary}; verified working)`;
|
|
1776
1789
|
deps.writeStdout(message);
|
|
1777
1790
|
return message;
|
|
1778
1791
|
}
|
|
@@ -68,66 +68,57 @@ function assessStability(payload, now) {
|
|
|
68
68
|
* Build an ANSI string for in-place terminal display during polling.
|
|
69
69
|
* Uses cursor-up and line-clear escapes to overwrite previous output.
|
|
70
70
|
*/
|
|
71
|
-
function renderStartupProgress(payload, elapsed, prevLineCount = 0) {
|
|
71
|
+
function renderStartupProgress(payload, elapsed, prevLineCount = 0, options = {}) {
|
|
72
|
+
const isTTY = options.isTTY ?? true;
|
|
72
73
|
const frameIndex = Math.floor(elapsed / 100) % SPINNER_FRAMES.length;
|
|
73
74
|
const spinner = SPINNER_FRAMES[frameIndex];
|
|
74
75
|
const lines = [];
|
|
75
76
|
const elapsedSec = (elapsed / 1000).toFixed(1);
|
|
76
|
-
lines.push(
|
|
77
|
+
lines.push(isTTY
|
|
78
|
+
? `${spinner} ${BOLD}waiting for agents${RESET} ${DIM}(${elapsedSec}s)${RESET}`
|
|
79
|
+
: `${spinner} waiting for agents (${elapsedSec}s)`);
|
|
77
80
|
for (const worker of payload.workers) {
|
|
78
|
-
const
|
|
79
|
-
: worker.status === "crashed" ? RED
|
|
80
|
-
: YELLOW;
|
|
81
|
-
const statusText = `${statusColor}${worker.status}${RESET}`;
|
|
81
|
+
const statusText = isTTY ? colorStatus(worker.status) : worker.status;
|
|
82
82
|
lines.push(` ${worker.agent}/${worker.worker}: ${statusText}`);
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
if (prevLineCount > 0) {
|
|
86
|
-
output += `\x1b[${prevLineCount}A`;
|
|
87
|
-
}
|
|
88
|
-
for (const line of lines) {
|
|
89
|
-
output += `\x1b[2K${line}\n`;
|
|
90
|
-
}
|
|
91
|
-
return output;
|
|
84
|
+
return renderStartupLines(lines, prevLineCount, isTTY);
|
|
92
85
|
}
|
|
93
86
|
/**
|
|
94
87
|
* Render a pre-socket status line showing what the daemon is doing.
|
|
95
88
|
*/
|
|
96
|
-
function renderWaitingForDaemon(elapsed, latestEvent, prevLineCount = 0) {
|
|
89
|
+
function renderWaitingForDaemon(elapsed, latestEvent, prevLineCount = 0, options = {}) {
|
|
90
|
+
const isTTY = options.isTTY ?? true;
|
|
97
91
|
const elapsedSec = (elapsed / 1000).toFixed(1);
|
|
98
92
|
const frameIndex = Math.floor(elapsed / 100) % SPINNER_FRAMES.length;
|
|
99
93
|
const spinner = SPINNER_FRAMES[frameIndex];
|
|
100
94
|
const lines = [];
|
|
101
|
-
lines.push(
|
|
95
|
+
lines.push(isTTY
|
|
96
|
+
? `${spinner} ${BOLD}waiting for daemon${RESET} ${DIM}(${elapsedSec}s)${RESET}`
|
|
97
|
+
: `${spinner} waiting for daemon (${elapsedSec}s)`);
|
|
102
98
|
if (latestEvent) {
|
|
103
|
-
lines.push(` ${DIM}${latestEvent}${RESET}`);
|
|
104
|
-
}
|
|
105
|
-
let output = "";
|
|
106
|
-
if (prevLineCount > 0) {
|
|
107
|
-
output += `\x1b[${prevLineCount}A`;
|
|
108
|
-
}
|
|
109
|
-
for (const line of lines) {
|
|
110
|
-
output += `\x1b[2K${line}\n`;
|
|
99
|
+
lines.push(isTTY ? ` ${DIM}${latestEvent}${RESET}` : ` ${latestEvent}`);
|
|
111
100
|
}
|
|
112
|
-
return
|
|
101
|
+
return renderStartupLines(lines, prevLineCount, isTTY);
|
|
113
102
|
}
|
|
114
103
|
/**
|
|
115
104
|
* Render the final summary after all agents have resolved.
|
|
116
105
|
*/
|
|
117
|
-
function renderFinalSummary(result) {
|
|
106
|
+
function renderFinalSummary(result, isTTY) {
|
|
118
107
|
const lines = [];
|
|
119
108
|
for (const agent of result.stable) {
|
|
120
|
-
lines.push(` ${GREEN}\u2713${RESET} ${agent}: ${GREEN}stable${RESET}`);
|
|
109
|
+
lines.push(isTTY ? ` ${GREEN}\u2713${RESET} ${agent}: ${GREEN}stable${RESET}` : ` \u2713 ${agent}: stable`);
|
|
121
110
|
}
|
|
122
111
|
for (const d of result.degraded) {
|
|
123
|
-
lines.push(` ${RED}\u2717${RESET} ${d.agent}: ${RED}degraded${RESET}`);
|
|
112
|
+
lines.push(isTTY ? ` ${RED}\u2717${RESET} ${d.agent}: ${RED}degraded${RESET}` : ` \u2717 ${d.agent}: degraded`);
|
|
124
113
|
if (d.errorReason !== "unknown error") {
|
|
125
|
-
lines.push(` ${DIM}error: ${d.errorReason}${RESET}`);
|
|
114
|
+
lines.push(isTTY ? ` ${DIM}error: ${d.errorReason}${RESET}` : ` error: ${d.errorReason}`);
|
|
126
115
|
}
|
|
127
116
|
if (d.fixHint !== "check daemon logs") {
|
|
128
|
-
lines.push(` ${DIM}fix: ${d.fixHint}${RESET}`);
|
|
117
|
+
lines.push(isTTY ? ` ${DIM}fix: ${d.fixHint}${RESET}` : ` fix: ${d.fixHint}`);
|
|
129
118
|
}
|
|
130
119
|
}
|
|
120
|
+
if (!isTTY)
|
|
121
|
+
return lines.join("\n") + "\n";
|
|
131
122
|
return lines.map((line) => `\x1b[2K${line}`).join("\n") + "\n";
|
|
132
123
|
}
|
|
133
124
|
// ── Polling loop ──
|
|
@@ -141,6 +132,7 @@ function renderFinalSummary(result) {
|
|
|
141
132
|
async function pollDaemonStartup(deps) {
|
|
142
133
|
const startTime = deps.now();
|
|
143
134
|
let prevLineCount = 0;
|
|
135
|
+
const isTTY = deps.isTTY ?? true;
|
|
144
136
|
const isAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
|
|
145
137
|
(0, runtime_1.emitNervesEvent)({
|
|
146
138
|
component: "daemon",
|
|
@@ -169,7 +161,7 @@ async function pollDaemonStartup(deps) {
|
|
|
169
161
|
meta: { pid: deps.daemonPid, lastEvent: latestEvent },
|
|
170
162
|
});
|
|
171
163
|
// Clear the waiting line
|
|
172
|
-
if (prevLineCount > 0) {
|
|
164
|
+
if (isTTY && prevLineCount > 0) {
|
|
173
165
|
let clear = `\x1b[${prevLineCount}A`;
|
|
174
166
|
for (let i = 0; i < prevLineCount; i++)
|
|
175
167
|
clear += `\x1b[2K\n`;
|
|
@@ -182,12 +174,12 @@ async function pollDaemonStartup(deps) {
|
|
|
182
174
|
}
|
|
183
175
|
// Show what the daemon is doing from its log
|
|
184
176
|
const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
|
|
185
|
-
const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount);
|
|
177
|
+
const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount, { isTTY });
|
|
186
178
|
deps.writeRaw(output);
|
|
187
179
|
prevLineCount = latestEvent ? 2 : 1;
|
|
188
180
|
}
|
|
189
181
|
if (payload) {
|
|
190
|
-
const output = renderStartupProgress(payload, elapsed, prevLineCount);
|
|
182
|
+
const output = renderStartupProgress(payload, elapsed, prevLineCount, { isTTY });
|
|
191
183
|
deps.writeRaw(output);
|
|
192
184
|
prevLineCount = payload.workers.length + 1;
|
|
193
185
|
const assessment = assessStability(payload, now);
|
|
@@ -196,7 +188,7 @@ async function pollDaemonStartup(deps) {
|
|
|
196
188
|
stable: assessment.stable,
|
|
197
189
|
degraded: assessment.degraded,
|
|
198
190
|
};
|
|
199
|
-
const summary = renderFinalSummary(result);
|
|
191
|
+
const summary = renderFinalSummary(result, isTTY);
|
|
200
192
|
deps.writeRaw(summary);
|
|
201
193
|
(0, runtime_1.emitNervesEvent)({
|
|
202
194
|
component: "daemon",
|
|
@@ -214,6 +206,24 @@ async function pollDaemonStartup(deps) {
|
|
|
214
206
|
await deps.sleep(POLL_INTERVAL_MS);
|
|
215
207
|
}
|
|
216
208
|
}
|
|
209
|
+
function colorStatus(status) {
|
|
210
|
+
const statusColor = status === "running" ? GREEN
|
|
211
|
+
: status === "crashed" ? RED
|
|
212
|
+
: YELLOW;
|
|
213
|
+
return `${statusColor}${status}${RESET}`;
|
|
214
|
+
}
|
|
215
|
+
function renderStartupLines(lines, prevLineCount, isTTY) {
|
|
216
|
+
if (!isTTY)
|
|
217
|
+
return lines.join("\n") + "\n";
|
|
218
|
+
let output = "";
|
|
219
|
+
if (prevLineCount > 0) {
|
|
220
|
+
output += `\x1b[${prevLineCount}A`;
|
|
221
|
+
}
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
output += `\x1b[2K${line}\n`;
|
|
224
|
+
}
|
|
225
|
+
return output;
|
|
226
|
+
}
|
|
217
227
|
/* v8 ignore start -- process liveness check: uses real process.kill(0), tested via deployment @preserve */
|
|
218
228
|
function defaultIsProcessAlive(pid) {
|
|
219
229
|
try {
|
|
@@ -42,6 +42,7 @@ const identity_1 = require("../identity");
|
|
|
42
42
|
const config_1 = require("../config");
|
|
43
43
|
const runtime_1 = require("../../nerves/runtime");
|
|
44
44
|
const auth_flow_1 = require("../auth/auth-flow");
|
|
45
|
+
const provider_models_1 = require("../provider-models");
|
|
45
46
|
const habit_parser_1 = require("../habits/habit-parser");
|
|
46
47
|
const hatch_specialist_1 = require("./hatch-specialist");
|
|
47
48
|
function requiredCredentialKeys(provider) {
|
|
@@ -124,9 +125,10 @@ function writeDiaryScaffold(bundleRoot) {
|
|
|
124
125
|
}
|
|
125
126
|
function writeHatchlingAgentConfig(bundleRoot, input) {
|
|
126
127
|
const template = (0, identity_1.buildDefaultAgentTemplate)(input.agentName);
|
|
128
|
+
const model = (0, provider_models_1.getDefaultModelForProvider)(input.provider);
|
|
127
129
|
template.provider = input.provider;
|
|
128
|
-
template.humanFacing = { provider: input.provider, model
|
|
129
|
-
template.agentFacing = { provider: input.provider, model
|
|
130
|
+
template.humanFacing = { provider: input.provider, model };
|
|
131
|
+
template.agentFacing = { provider: input.provider, model };
|
|
130
132
|
template.enabled = true;
|
|
131
133
|
fs.writeFileSync(path.join(bundleRoot, "agent.json"), `${JSON.stringify(template, null, 2)}\n`, "utf-8");
|
|
132
134
|
}
|
|
@@ -2,15 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildFailoverContext = buildFailoverContext;
|
|
4
4
|
exports.handleFailoverReply = handleFailoverReply;
|
|
5
|
+
const provider_models_1 = require("./provider-models");
|
|
5
6
|
const runtime_1 = require("../nerves/runtime");
|
|
6
|
-
const FAILING_PROVIDER_LABELS = {
|
|
7
|
-
"auth-failure": "its credentials need to be refreshed",
|
|
8
|
-
"usage-limit": "has also hit its usage limit",
|
|
9
|
-
"rate-limit": "is also being rate limited",
|
|
10
|
-
"server-error": "is also experiencing an outage",
|
|
11
|
-
"network-error": "could not be reached",
|
|
12
|
-
"unknown": "could not be reached",
|
|
13
|
-
};
|
|
14
7
|
const CLASSIFICATION_LABELS = {
|
|
15
8
|
"auth-failure": "authentication failed",
|
|
16
9
|
"usage-limit": "hit its usage limit",
|
|
@@ -19,10 +12,43 @@ const CLASSIFICATION_LABELS = {
|
|
|
19
12
|
"network-error": "is unreachable (network error)",
|
|
20
13
|
"unknown": "encountered an error",
|
|
21
14
|
};
|
|
22
|
-
function
|
|
15
|
+
function formatProviderWithModel(provider, model) {
|
|
16
|
+
if (!model)
|
|
17
|
+
return provider;
|
|
18
|
+
if ((0, provider_models_1.getProviderModelMismatchMessage)(provider, model)) {
|
|
19
|
+
return `${provider} [configured model: ${model}]`;
|
|
20
|
+
}
|
|
21
|
+
return `${provider} (${model})`;
|
|
22
|
+
}
|
|
23
|
+
function formatErrorDetail(errorMessage, errorSummary) {
|
|
24
|
+
const detail = errorMessage.replace(/\s+/g, " ").trim();
|
|
25
|
+
if (!detail || detail === errorSummary)
|
|
26
|
+
return "";
|
|
27
|
+
return detail.length > 300 ? `${detail.slice(0, 297)}...` : detail;
|
|
28
|
+
}
|
|
29
|
+
function formatFailingProviderLine(provider, classification, agentName) {
|
|
30
|
+
const authCommand = `ouro auth --agent ${agentName} --provider ${provider}`;
|
|
31
|
+
switch (classification) {
|
|
32
|
+
case "auth-failure":
|
|
33
|
+
return ` - ${provider}: credentials need to be refreshed. Run \`${authCommand}\`.`;
|
|
34
|
+
case "network-error":
|
|
35
|
+
return ` - ${provider}: could not be reached. Check network/provider availability; if credentials may be stale, run \`${authCommand}\`.`;
|
|
36
|
+
case "server-error":
|
|
37
|
+
return ` - ${provider}: provider outage or server error. Retry later; if it keeps failing, run \`${authCommand}\`.`;
|
|
38
|
+
case "rate-limit":
|
|
39
|
+
return ` - ${provider}: rate limited. Wait and retry, or switch to a ready provider below.`;
|
|
40
|
+
case "usage-limit":
|
|
41
|
+
return ` - ${provider}: usage limit hit. Wait for quota reset, raise quota, or switch to a ready provider below.`;
|
|
42
|
+
case "unknown":
|
|
43
|
+
return ` - ${provider}: could not be reached. Run \`${authCommand}\` if credentials may be stale.`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function buildFailoverContext(errorMessage, classification, currentProvider, currentModel, agentName, inventory, providerModels) {
|
|
23
47
|
const label = CLASSIFICATION_LABELS[classification];
|
|
24
|
-
const providerWithModel =
|
|
48
|
+
const providerWithModel = formatProviderWithModel(currentProvider, currentModel);
|
|
25
49
|
const errorSummary = `${providerWithModel} ${label}`;
|
|
50
|
+
const errorDetail = formatErrorDetail(errorMessage, errorSummary);
|
|
51
|
+
const modelMismatch = (0, provider_models_1.getProviderModelMismatchMessage)(currentProvider, currentModel);
|
|
26
52
|
const workingProviders = [];
|
|
27
53
|
const unconfiguredProviders = [];
|
|
28
54
|
const failingProviders = [];
|
|
@@ -39,27 +65,48 @@ function buildFailoverContext(_errorMessage, classification, currentProvider, cu
|
|
|
39
65
|
}
|
|
40
66
|
}
|
|
41
67
|
const lines = [`${errorSummary}.`];
|
|
68
|
+
if (errorDetail) {
|
|
69
|
+
lines.push(`provider detail: ${errorDetail}`);
|
|
70
|
+
}
|
|
71
|
+
if (classification === "auth-failure") {
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push("To keep using the current provider:");
|
|
74
|
+
lines.push(` 1. Run \`ouro auth --agent ${agentName} --provider ${currentProvider}\``);
|
|
75
|
+
}
|
|
76
|
+
if (modelMismatch) {
|
|
77
|
+
const defaultModel = (0, provider_models_1.getDefaultModelForProvider)(currentProvider);
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push("Config warning:");
|
|
80
|
+
lines.push(` - ${modelMismatch}`);
|
|
81
|
+
lines.push(" - Repair the configured model with:");
|
|
82
|
+
lines.push(` \`ouro config model --agent ${agentName} --facing human ${defaultModel}\``);
|
|
83
|
+
lines.push(` \`ouro config model --agent ${agentName} --facing agent ${defaultModel}\``);
|
|
84
|
+
}
|
|
42
85
|
if (workingProviders.length > 0) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
lines.push(`reply ${switchOptions} to continue.`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push("Ready providers:");
|
|
88
|
+
for (const provider of workingProviders) {
|
|
89
|
+
const model = (0, provider_models_1.resolveModelForProviderDisplay)(provider, providerModels[provider]);
|
|
90
|
+
lines.push(` - ${provider} (${model}): reply "switch to ${provider}"`);
|
|
91
|
+
}
|
|
50
92
|
}
|
|
51
93
|
if (failingProviders.length > 0) {
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("Configured but unavailable:");
|
|
52
96
|
for (const { provider, classification } of failingProviders) {
|
|
53
|
-
|
|
54
|
-
const detail = FAILING_PROVIDER_LABELS[classification] ?? "could not be reached";
|
|
55
|
-
lines.push(`${provider} is configured but ${detail}. run \`ouro auth --agent ${agentName} --provider ${provider}\` to refresh.`);
|
|
97
|
+
lines.push(formatFailingProviderLine(provider, classification, agentName));
|
|
56
98
|
}
|
|
57
99
|
}
|
|
58
100
|
if (unconfiguredProviders.length > 0) {
|
|
59
|
-
lines.push(
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push("Not configured:");
|
|
103
|
+
for (const provider of unconfiguredProviders) {
|
|
104
|
+
lines.push(` - ${provider}: run \`ouro auth --agent ${agentName} --provider ${provider}\``);
|
|
105
|
+
}
|
|
60
106
|
}
|
|
61
107
|
if (workingProviders.length === 0 && unconfiguredProviders.length === 0 && failingProviders.length === 0) {
|
|
62
|
-
lines.push(
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push(`No other providers are available. Run \`ouro auth --agent ${agentName}\` in terminal to configure one.`);
|
|
63
110
|
}
|
|
64
111
|
(0, runtime_1.emitNervesEvent)({
|
|
65
112
|
component: "engine",
|
|
@@ -74,7 +121,7 @@ function buildFailoverContext(_errorMessage, classification, currentProvider, cu
|
|
|
74
121
|
agentName,
|
|
75
122
|
workingProviders,
|
|
76
123
|
unconfiguredProviders,
|
|
77
|
-
userMessage: lines.join("
|
|
124
|
+
userMessage: lines.join("\n"),
|
|
78
125
|
};
|
|
79
126
|
}
|
|
80
127
|
function handleFailoverReply(reply, context) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PROVIDER_MODELS = void 0;
|
|
4
|
+
exports.getProviderDisplayName = getProviderDisplayName;
|
|
5
|
+
exports.getDefaultModelForProvider = getDefaultModelForProvider;
|
|
6
|
+
exports.isModelClearlyIncompatibleWithProvider = isModelClearlyIncompatibleWithProvider;
|
|
7
|
+
exports.resolveModelForProviderSelection = resolveModelForProviderSelection;
|
|
8
|
+
exports.resolveModelForProviderDisplay = resolveModelForProviderDisplay;
|
|
9
|
+
exports.getProviderModelMismatchMessage = getProviderModelMismatchMessage;
|
|
10
|
+
const runtime_1 = require("../nerves/runtime");
|
|
11
|
+
exports.DEFAULT_PROVIDER_MODELS = {
|
|
12
|
+
anthropic: "claude-opus-4-6",
|
|
13
|
+
azure: "gpt-4o-mini",
|
|
14
|
+
minimax: "MiniMax-M2.7",
|
|
15
|
+
"openai-codex": "gpt-5.4",
|
|
16
|
+
"github-copilot": "claude-sonnet-4.6",
|
|
17
|
+
};
|
|
18
|
+
const PROVIDER_NAMES = {
|
|
19
|
+
anthropic: "Anthropic",
|
|
20
|
+
azure: "Azure OpenAI",
|
|
21
|
+
minimax: "MiniMax",
|
|
22
|
+
"openai-codex": "OpenAI Codex",
|
|
23
|
+
"github-copilot": "GitHub Copilot",
|
|
24
|
+
};
|
|
25
|
+
function normalized(model) {
|
|
26
|
+
return model.trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
function getProviderDisplayName(provider) {
|
|
29
|
+
return PROVIDER_NAMES[provider];
|
|
30
|
+
}
|
|
31
|
+
function getDefaultModelForProvider(provider) {
|
|
32
|
+
return exports.DEFAULT_PROVIDER_MODELS[provider];
|
|
33
|
+
}
|
|
34
|
+
function isModelClearlyIncompatibleWithProvider(provider, model) {
|
|
35
|
+
const value = normalized(model);
|
|
36
|
+
if (!value)
|
|
37
|
+
return true;
|
|
38
|
+
switch (provider) {
|
|
39
|
+
case "anthropic":
|
|
40
|
+
return !value.startsWith("claude-");
|
|
41
|
+
case "minimax":
|
|
42
|
+
return !value.startsWith("minimax");
|
|
43
|
+
case "openai-codex":
|
|
44
|
+
return value.startsWith("claude-") || value.startsWith("minimax");
|
|
45
|
+
case "azure":
|
|
46
|
+
return value.startsWith("claude-") || value.startsWith("minimax");
|
|
47
|
+
case "github-copilot":
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveModelForProviderSelection(provider, currentModel) {
|
|
52
|
+
const trimmed = currentModel.trim();
|
|
53
|
+
if (trimmed && !isModelClearlyIncompatibleWithProvider(provider, trimmed)) {
|
|
54
|
+
return { model: trimmed, preserved: true };
|
|
55
|
+
}
|
|
56
|
+
const model = getDefaultModelForProvider(provider);
|
|
57
|
+
(0, runtime_1.emitNervesEvent)({
|
|
58
|
+
component: "config/identity",
|
|
59
|
+
event: "config_identity.provider_model_defaulted",
|
|
60
|
+
message: "defaulted provider model during provider selection",
|
|
61
|
+
meta: { provider, previousModel: currentModel, model },
|
|
62
|
+
});
|
|
63
|
+
return { model, preserved: false };
|
|
64
|
+
}
|
|
65
|
+
function resolveModelForProviderDisplay(provider, modelHint) {
|
|
66
|
+
const hint = modelHint?.trim() ?? "";
|
|
67
|
+
if (hint && !isModelClearlyIncompatibleWithProvider(provider, hint))
|
|
68
|
+
return hint;
|
|
69
|
+
return getDefaultModelForProvider(provider);
|
|
70
|
+
}
|
|
71
|
+
function getProviderModelMismatchMessage(provider, model) {
|
|
72
|
+
const trimmed = model.trim();
|
|
73
|
+
if (!isModelClearlyIncompatibleWithProvider(provider, trimmed))
|
|
74
|
+
return null;
|
|
75
|
+
const providerName = getProviderDisplayName(provider);
|
|
76
|
+
const defaultModel = getDefaultModelForProvider(provider);
|
|
77
|
+
if (!trimmed) {
|
|
78
|
+
return `${providerName} has no model set. Suggested model: ${defaultModel}.`;
|
|
79
|
+
}
|
|
80
|
+
return `${providerName} is currently paired with ${trimmed}, which does not look like a model for ${providerName}. Suggested model: ${defaultModel}.`;
|
|
81
|
+
}
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -54,6 +54,7 @@ const obligations_1 = require("../arc/obligations");
|
|
|
54
54
|
const provider_failover_1 = require("../heart/provider-failover");
|
|
55
55
|
const provider_ping_1 = require("../heart/provider-ping");
|
|
56
56
|
const auth_flow_1 = require("../heart/auth/auth-flow");
|
|
57
|
+
const provider_models_1 = require("../heart/provider-models");
|
|
57
58
|
const tempo_1 = require("../heart/tempo");
|
|
58
59
|
const temporal_view_1 = require("../heart/temporal-view");
|
|
59
60
|
const start_of_turn_packet_1 = require("../heart/start-of-turn-packet");
|
|
@@ -162,14 +163,15 @@ async function handleInboundTurn(input) {
|
|
|
162
163
|
try {
|
|
163
164
|
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
|
|
164
165
|
const cfg = secrets.providers[failoverAction.provider];
|
|
165
|
-
|
|
166
|
+
const hint = cfg?.model ?? cfg?.modelName;
|
|
167
|
+
return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider, typeof hint === "string" ? hint : "");
|
|
166
168
|
/* v8 ignore next 2 -- defensive: secrets read failure @preserve */
|
|
167
169
|
}
|
|
168
170
|
catch {
|
|
169
|
-
return
|
|
171
|
+
return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider);
|
|
170
172
|
}
|
|
171
173
|
})();
|
|
172
|
-
const newProviderLabel =
|
|
174
|
+
const newProviderLabel = `${failoverAction.provider} (${newProviderSecrets})`;
|
|
173
175
|
input.messages = [{
|
|
174
176
|
role: "user",
|
|
175
177
|
content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.328",
|
|
4
4
|
"main": "dist/heart/daemon/ouro-entry.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cli": "dist/heart/daemon/ouro-bot-entry.js",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"test:coverage": "node scripts/run-coverage-gate.cjs",
|
|
32
32
|
"build": "tsc && (cd packages/outlook-ui && npm install --ignore-scripts 2>/dev/null && npm run build && cp -r dist ../../dist/outlook-ui) || echo 'outlook-ui build skipped'",
|
|
33
33
|
"lint": "eslint src/",
|
|
34
|
+
"release:smoke": "node scripts/release-smoke.cjs",
|
|
34
35
|
"audit:nerves": "npm run build && node dist/nerves/coverage/cli-main.js"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|