@ouro.bot/cli 0.1.0-alpha.352 → 0.1.0-alpha.354
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/README.md +17 -8
- package/changelog.json +23 -0
- package/dist/heart/daemon/cli-render.js +50 -2
- package/dist/heart/daemon/daemon.js +6 -0
- package/dist/heart/daemon/pulse.js +15 -3
- package/dist/heart/outlook/outlook-view.js +1 -0
- package/dist/heart/outlook/readers/agent-machine.js +4 -0
- package/dist/heart/provider-failover.js +163 -24
- package/dist/heart/provider-visibility.js +183 -0
- package/dist/heart/start-of-turn-packet.js +4 -0
- package/dist/heart/turn-context.js +4 -0
- package/dist/mind/prompt.js +12 -2
- package/dist/senses/pipeline.js +72 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@ Ouroboros is a TypeScript harness for daemon-managed agents that live in externa
|
|
|
11
11
|
- `ouro up` starts the daemon from the installed production version, syncs the launcher, installs workflow helpers, and reconciles stale runtime state.
|
|
12
12
|
- `ouro dev` starts the daemon from a local repo build. It auto-builds from source, disables launchd auto-restart (so the installed daemon doesn't respawn underneath you), persists the repo path in `~/.ouro-cli/dev-config.json` for next time, and force-restarts the daemon. If you run `ouro dev` from inside the repo, it detects the CWD automatically. Run `ouro up` to return to production mode (this also cleans up `dev-config.json`).
|
|
13
13
|
- Agent bundles live outside the repo at `~/AgentBundles/<agent>.ouro/`.
|
|
14
|
-
-
|
|
14
|
+
- Provider credentials live outside the repo at `~/.agentsecrets/providers.json`.
|
|
15
|
+
- Sense-specific secrets live outside the repo at `~/.agentsecrets/<agent>/secrets.json`.
|
|
15
16
|
- Machine-scoped test and runtime spillover lives under `~/.agentstate/...`.
|
|
16
17
|
|
|
17
18
|
Current first-class senses:
|
|
@@ -90,8 +91,10 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
|
|
|
90
91
|
|
|
91
92
|
## Runtime Truths
|
|
92
93
|
|
|
93
|
-
- `agent.json` is the source of truth for
|
|
94
|
-
- `configPath` must point to `~/.agentsecrets/<agent>/secrets.json
|
|
94
|
+
- `agent.json` is the source of truth for phrase pools, context settings, enabled senses, and the agent's `configPath`. Legacy `humanFacing`/`agentFacing` values are bootstrap inputs, not live machine fallback.
|
|
95
|
+
- `configPath` must point to `~/.agentsecrets/<agent>/secrets.json` for sense-specific secrets.
|
|
96
|
+
- `state/providers.json` is the local source of truth for provider+model selection on this machine. It has two lanes: `outward` for CLI, Teams, and BlueBubbles turns, and `inner` for inner dialogue.
|
|
97
|
+
- `~/.agentsecrets/providers.json` is the machine credential pool. It stores provider credentials only, records which agent contributed them, and is shared by all agents on that machine.
|
|
95
98
|
- The daemon discovers bundles dynamically from `~/AgentBundles`.
|
|
96
99
|
- `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
|
|
97
100
|
- `bundle-meta.json` tracks the runtime version that last touched a bundle.
|
|
@@ -104,17 +107,22 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
|
|
|
104
107
|
- `running`
|
|
105
108
|
- `error`
|
|
106
109
|
|
|
107
|
-
When a model provider needs first-time setup
|
|
110
|
+
When a model provider needs first-time setup or reauth, use:
|
|
108
111
|
|
|
109
112
|
```bash
|
|
110
113
|
ouro auth --agent <name>
|
|
111
114
|
ouro auth --agent <name> --provider <provider>
|
|
112
|
-
ouro auth --agent <name> --facing agent --provider <provider>
|
|
113
115
|
```
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
`ouro auth` stores credentials only. It does not switch a lane or write provider/model selection.
|
|
118
|
+
|
|
119
|
+
When you want this machine to use a provider/model for a lane, use:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ouro use --agent <name> --lane <outward|inner> --provider <provider> --model <model>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The outward lane handles user-facing senses. The inner lane handles the agent's private thinking. `ouro use` performs the provider/model check before committing the lane, so a broken local choice fails fast with a repair path instead of surprising the next turn.
|
|
118
126
|
|
|
119
127
|
## Quickstart
|
|
120
128
|
|
|
@@ -159,6 +167,7 @@ ouro logs
|
|
|
159
167
|
ouro stop
|
|
160
168
|
ouro auth --agent <name>
|
|
161
169
|
ouro auth --agent <name> --provider <provider>
|
|
170
|
+
ouro use --agent <name> --lane <outward|inner> --provider <provider> --model <model>
|
|
162
171
|
ouro hatch
|
|
163
172
|
ouro chat <agent>
|
|
164
173
|
ouro msg --to <agent> [--session <id>] [--task <ref>] <message>
|
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.354",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Provider visibility now surfaces the effective local outward/inner lanes across start-of-turn context, system prompts, pulse, Outlook, and daemon status, using the shared provider binding resolver with safe credential provenance and no raw secret exposure.",
|
|
8
|
+
"Provider auth docs now state the runtime model plainly: `state/providers.json` selects provider/model per machine, `~/.agentsecrets/providers.json` stores machine credentials, `ouro use` switches lanes, and `ouro auth` stores credentials only."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"version": "0.1.0-alpha.353",
|
|
13
|
+
"changes": [
|
|
14
|
+
"Provider failover now uses the machine-wide credential pool and local provider-state lanes: terminal errors build ready-provider choices with safe credential provenance, `switch to <provider>` carries lane/model/revision context, and the retry path updates only the failed `state/providers.json` lane instead of mutating both synced `agent.json` facings."
|
|
15
|
+
]
|
|
16
|
+
},
|
|
4
17
|
{
|
|
5
18
|
"version": "0.1.0-alpha.352",
|
|
6
19
|
"changes": [
|
|
@@ -9,6 +22,16 @@
|
|
|
9
22
|
"Autonomous execution prompt contract added: when told to work autonomously, agents use ponder to absorb new messages and continue using tools, settling only with the final result."
|
|
10
23
|
]
|
|
11
24
|
},
|
|
25
|
+
{
|
|
26
|
+
"version": "0.1.0-alpha.351",
|
|
27
|
+
"changes": [
|
|
28
|
+
"Surface tool description rewritten from 'surface progress' to 'send a message to someone' — makes it clear the tool is for interpersonal messaging, not status reporting.",
|
|
29
|
+
"Inner dialog prompt contract now guides agents to use rest(note) for heartbeat state and ponder(reflection) for deeper thoughts, keeping surface strictly for words meant for another person.",
|
|
30
|
+
"Removed [surfaced from inner dialog] prefix from synthetic session messages — provenance is tracked via captureKind: 'synthetic', the prefix was redundant and created echo loops.",
|
|
31
|
+
"Obligation summaries and attention queue headers reframed as structured internal data ([internal] tags) instead of surface-ready prose.",
|
|
32
|
+
"Shared proactive-content-guard module blocks internal content (heartbeat, check-in, task board, obligation status, meta markers) from BlueBubbles and Teams proactive sends."
|
|
33
|
+
]
|
|
34
|
+
},
|
|
12
35
|
{
|
|
13
36
|
"version": "0.1.0-alpha.350",
|
|
14
37
|
"changes": [
|
|
@@ -75,15 +75,18 @@ function parseStatusPayload(data) {
|
|
|
75
75
|
const workers = raw.workers;
|
|
76
76
|
const sync = raw.sync;
|
|
77
77
|
const agents = raw.agents;
|
|
78
|
+
const providers = raw.providers;
|
|
78
79
|
if (!overview || typeof overview !== "object" || Array.isArray(overview))
|
|
79
80
|
return null;
|
|
80
81
|
if (!Array.isArray(senses) || !Array.isArray(workers))
|
|
81
82
|
return null;
|
|
82
|
-
// sync and
|
|
83
|
+
// sync, agents, and providers are optional for backward compatibility — older daemons may omit them
|
|
83
84
|
if (sync !== undefined && !Array.isArray(sync))
|
|
84
85
|
return null;
|
|
85
86
|
if (agents !== undefined && !Array.isArray(agents))
|
|
86
87
|
return null;
|
|
88
|
+
if (providers !== undefined && !Array.isArray(providers))
|
|
89
|
+
return null;
|
|
87
90
|
const parsedOverview = {
|
|
88
91
|
daemon: stringField(overview.daemon) ?? "unknown",
|
|
89
92
|
health: stringField(overview.health) ?? "unknown",
|
|
@@ -172,10 +175,38 @@ function parseStatusPayload(data) {
|
|
|
172
175
|
return null;
|
|
173
176
|
return { name, enabled };
|
|
174
177
|
});
|
|
178
|
+
const parsedProviders = (providers ?? []).map((entry) => {
|
|
179
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
180
|
+
return null;
|
|
181
|
+
const row = entry;
|
|
182
|
+
const agent = stringField(row.agent);
|
|
183
|
+
const lane = stringField(row.lane);
|
|
184
|
+
const provider = stringField(row.provider);
|
|
185
|
+
const model = stringField(row.model);
|
|
186
|
+
const source = stringField(row.source);
|
|
187
|
+
const readiness = stringField(row.readiness);
|
|
188
|
+
const credential = stringField(row.credential);
|
|
189
|
+
if (!agent || !lane || !provider || !model || !source || !readiness || !credential)
|
|
190
|
+
return null;
|
|
191
|
+
const parsed = {
|
|
192
|
+
agent,
|
|
193
|
+
lane,
|
|
194
|
+
provider,
|
|
195
|
+
model,
|
|
196
|
+
source,
|
|
197
|
+
readiness,
|
|
198
|
+
credential,
|
|
199
|
+
};
|
|
200
|
+
const detail = stringField(row.detail);
|
|
201
|
+
if (detail !== null)
|
|
202
|
+
parsed.detail = detail;
|
|
203
|
+
return parsed;
|
|
204
|
+
});
|
|
175
205
|
if (parsedSenses.some((row) => row === null) ||
|
|
176
206
|
parsedWorkers.some((row) => row === null) ||
|
|
177
207
|
parsedSync.some((row) => row === null) ||
|
|
178
|
-
parsedAgents.some((row) => row === null)
|
|
208
|
+
parsedAgents.some((row) => row === null) ||
|
|
209
|
+
parsedProviders.some((row) => row === null))
|
|
179
210
|
return null;
|
|
180
211
|
return {
|
|
181
212
|
overview: parsedOverview,
|
|
@@ -183,6 +214,7 @@ function parseStatusPayload(data) {
|
|
|
183
214
|
workers: parsedWorkers,
|
|
184
215
|
sync: parsedSync,
|
|
185
216
|
agents: parsedAgents,
|
|
217
|
+
providers: parsedProviders,
|
|
186
218
|
};
|
|
187
219
|
}
|
|
188
220
|
// ── ANSI color helpers (private) ──
|
|
@@ -208,12 +240,15 @@ function statusDot(status) {
|
|
|
208
240
|
case "ok":
|
|
209
241
|
case "interactive":
|
|
210
242
|
case "enabled":
|
|
243
|
+
case "ready":
|
|
211
244
|
return green("●");
|
|
212
245
|
case "crashed":
|
|
213
246
|
case "warn":
|
|
214
247
|
case "error":
|
|
248
|
+
case "failed":
|
|
215
249
|
return red("●");
|
|
216
250
|
case "needs_config":
|
|
251
|
+
case "stale":
|
|
217
252
|
return yellow("●");
|
|
218
253
|
case "disabled":
|
|
219
254
|
case "stopped":
|
|
@@ -291,6 +326,18 @@ function formatDaemonStatusOutput(response, fallback) {
|
|
|
291
326
|
}
|
|
292
327
|
lines.push("");
|
|
293
328
|
}
|
|
329
|
+
// ── Providers ──
|
|
330
|
+
if (payload.providers.length > 0) {
|
|
331
|
+
lines.push(` ${teal("──")} ${bold("Providers")} ${teal("─".repeat(34))}`);
|
|
332
|
+
const agentLaneWidth = Math.max(16, ...payload.providers.map((r) => `${r.agent} ${r.lane}`.length));
|
|
333
|
+
for (const row of payload.providers) {
|
|
334
|
+
const agentLane = `${row.agent} ${row.lane}`.padEnd(agentLaneWidth);
|
|
335
|
+
const model = `${row.provider} / ${row.model}`;
|
|
336
|
+
const detail = [row.readiness, row.detail, row.source, row.credential].filter(Boolean).join("; ");
|
|
337
|
+
lines.push(` ${agentLane} ${statusDot(row.readiness)} ${model} ${dim(detail)}`);
|
|
338
|
+
}
|
|
339
|
+
lines.push("");
|
|
340
|
+
}
|
|
294
341
|
// ── Senses ──
|
|
295
342
|
if (payload.senses.length > 0) {
|
|
296
343
|
lines.push(` ${teal("──")} ${bold("Senses")} ${teal("─".repeat(37))}`);
|
|
@@ -421,6 +468,7 @@ function buildStoppedStatusPayload(socketPath, syncRows = [], agentRows = []) {
|
|
|
421
468
|
workers: [],
|
|
422
469
|
sync: syncRows,
|
|
423
470
|
agents: agentRows,
|
|
471
|
+
providers: [],
|
|
424
472
|
};
|
|
425
473
|
}
|
|
426
474
|
function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
|
|
@@ -64,6 +64,7 @@ const outlook_http_1 = require("../outlook/outlook-http");
|
|
|
64
64
|
const outlook_types_1 = require("../outlook/outlook-types");
|
|
65
65
|
const outlook_read_1 = require("../outlook/outlook-read");
|
|
66
66
|
const outlook_view_1 = require("../outlook/outlook-view");
|
|
67
|
+
const provider_visibility_1 = require("../provider-visibility");
|
|
67
68
|
const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
|
|
68
69
|
/**
|
|
69
70
|
* Defense-in-depth: detect if we're running under vitest. The pidfile lives
|
|
@@ -427,6 +428,10 @@ class OuroDaemon {
|
|
|
427
428
|
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
428
429
|
const sync = (0, agent_discovery_1.listBundleSyncRows)({ bundlesRoot: this.bundlesRoot });
|
|
429
430
|
const agents = (0, agent_discovery_1.listAllBundleAgents)({ bundlesRoot: this.bundlesRoot });
|
|
431
|
+
const providers = agents.flatMap((agent) => (0, provider_visibility_1.providerVisibilityStatusRows)((0, provider_visibility_1.buildAgentProviderVisibility)({
|
|
432
|
+
agentName: agent.name,
|
|
433
|
+
agentRoot: path.join(this.bundlesRoot, `${agent.name}.ouro`),
|
|
434
|
+
})));
|
|
430
435
|
return {
|
|
431
436
|
overview: {
|
|
432
437
|
daemon: "running",
|
|
@@ -443,6 +448,7 @@ class OuroDaemon {
|
|
|
443
448
|
senses,
|
|
444
449
|
sync,
|
|
445
450
|
agents,
|
|
451
|
+
...(providers.length > 0 ? { providers } : {}),
|
|
446
452
|
};
|
|
447
453
|
}
|
|
448
454
|
async start() {
|
|
@@ -52,6 +52,7 @@ const fs = __importStar(require("fs"));
|
|
|
52
52
|
const os = __importStar(require("os"));
|
|
53
53
|
const path = __importStar(require("path"));
|
|
54
54
|
const runtime_1 = require("../../nerves/runtime");
|
|
55
|
+
const provider_visibility_1 = require("../provider-visibility");
|
|
55
56
|
/* v8 ignore next 3 -- path defaults: tests always inject @preserve */
|
|
56
57
|
function defaultPulsePath() {
|
|
57
58
|
return path.join(os.homedir(), ".ouro-cli", "pulse.json");
|
|
@@ -136,7 +137,7 @@ function readAgentActivity(bundlePath, readFile = (p) => fs.readFileSync(p, "utf
|
|
|
136
137
|
* directly, we just compute where it lives so sibling agents have a
|
|
137
138
|
* starting point if they want to navigate there manually.
|
|
138
139
|
*/
|
|
139
|
-
function buildPulseState(snapshots, bundlesRoot, daemonVersion, now, readActivity = readAgentActivity) {
|
|
140
|
+
function buildPulseState(snapshots, bundlesRoot, daemonVersion, now, readActivity = readAgentActivity, readProviderVisibility = () => null) {
|
|
140
141
|
const agents = snapshots.map((snap) => {
|
|
141
142
|
const errorReason = snap.errorReason;
|
|
142
143
|
const bundlePath = path.join(bundlesRoot, `${snap.name}.ouro`);
|
|
@@ -151,6 +152,7 @@ function buildPulseState(snapshots, bundlesRoot, daemonVersion, now, readActivit
|
|
|
151
152
|
// Only read activity for agents that are actually running. For
|
|
152
153
|
// crashed/stopped agents, the runtime.json is stale at best.
|
|
153
154
|
currentActivity: snap.status === "running" ? readActivity(bundlePath) : null,
|
|
155
|
+
providerVisibility: readProviderVisibility(snap.name, bundlePath),
|
|
154
156
|
};
|
|
155
157
|
});
|
|
156
158
|
return {
|
|
@@ -294,7 +296,17 @@ function readPulse(deps = {}) {
|
|
|
294
296
|
return {
|
|
295
297
|
generatedAt: parsed.generatedAt,
|
|
296
298
|
daemonVersion: parsed.daemonVersion,
|
|
297
|
-
agents: parsed.agents.filter(isValidPulseAgentEntry)
|
|
299
|
+
agents: parsed.agents.filter(isValidPulseAgentEntry).map((agent) => {
|
|
300
|
+
const rawAgent = agent;
|
|
301
|
+
if (!Object.prototype.hasOwnProperty.call(rawAgent, "providerVisibility"))
|
|
302
|
+
return agent;
|
|
303
|
+
return {
|
|
304
|
+
...agent,
|
|
305
|
+
providerVisibility: (0, provider_visibility_1.isAgentProviderVisibility)(rawAgent.providerVisibility)
|
|
306
|
+
? rawAgent.providerVisibility
|
|
307
|
+
: null,
|
|
308
|
+
};
|
|
309
|
+
}),
|
|
298
310
|
};
|
|
299
311
|
}
|
|
300
312
|
catch {
|
|
@@ -390,7 +402,7 @@ function pruneDeliveredState(delivered, state) {
|
|
|
390
402
|
* for production callers.
|
|
391
403
|
*/
|
|
392
404
|
function flushPulse(deps) {
|
|
393
|
-
const state = buildPulseState(deps.snapshots, deps.bundlesRoot, deps.daemonVersion, deps.now);
|
|
405
|
+
const state = buildPulseState(deps.snapshots, deps.bundlesRoot, deps.daemonVersion, deps.now, readAgentActivity, (agentName, bundlePath) => (0, provider_visibility_1.buildAgentProviderVisibility)({ agentName, agentRoot: bundlePath }));
|
|
394
406
|
/* v8 ignore start -- dep defaults: production daemon path; the arrow functions only fire when the corresponding dep is omitted, which only happens in production code paths. Tests inject all deps explicitly. @preserve */
|
|
395
407
|
const readPrev = deps.readPrev ?? (() => readPulse());
|
|
396
408
|
const writeNext = deps.writeNext ?? ((s) => writePulse(s));
|
|
@@ -173,6 +173,7 @@ function buildOutlookAgentView(input) {
|
|
|
173
173
|
agentRoot: input.agent.agentRoot,
|
|
174
174
|
enabled: input.agent.enabled,
|
|
175
175
|
provider: input.agent.provider,
|
|
176
|
+
providers: input.agent.providers ?? null,
|
|
176
177
|
senses: input.agent.senses,
|
|
177
178
|
freshness: input.agent.freshness,
|
|
178
179
|
degraded: input.agent.degraded,
|
|
@@ -46,6 +46,7 @@ const session_activity_1 = require("../../session-activity");
|
|
|
46
46
|
const identity_1 = require("../../identity");
|
|
47
47
|
const agent_discovery_1 = require("../../daemon/agent-discovery");
|
|
48
48
|
const runtime_metadata_1 = require("../../daemon/runtime-metadata");
|
|
49
|
+
const provider_visibility_1 = require("../../provider-visibility");
|
|
49
50
|
const thoughts_1 = require("../../daemon/thoughts");
|
|
50
51
|
const outlook_types_1 = require("../outlook-types");
|
|
51
52
|
const shared_1 = require("./shared");
|
|
@@ -266,6 +267,7 @@ function summarizeAgent(state) {
|
|
|
266
267
|
return {
|
|
267
268
|
agentName: state.agentName,
|
|
268
269
|
enabled: state.enabled,
|
|
270
|
+
providers: state.providers,
|
|
269
271
|
freshness: state.freshness,
|
|
270
272
|
degraded: state.degraded,
|
|
271
273
|
tasks: {
|
|
@@ -296,6 +298,7 @@ function readOutlookAgentState(agentName, options = {}) {
|
|
|
296
298
|
issues.push(...inner.issues);
|
|
297
299
|
const coding = readCodingSummary(agentRoot);
|
|
298
300
|
issues.push(...coding.issues);
|
|
301
|
+
const providers = (0, provider_visibility_1.buildAgentProviderVisibility)({ agentName, agentRoot, homeDir: options.homeDir });
|
|
299
302
|
const latestActivityAt = collectLatestActivityTimestamps({
|
|
300
303
|
obligations: obligations.items,
|
|
301
304
|
sessions: sessions.items,
|
|
@@ -308,6 +311,7 @@ function readOutlookAgentState(agentName, options = {}) {
|
|
|
308
311
|
agentRoot,
|
|
309
312
|
enabled: config.summary.enabled,
|
|
310
313
|
provider: config.summary.provider,
|
|
314
|
+
providers,
|
|
311
315
|
senses: config.summary.senses,
|
|
312
316
|
freshness: summarizeFreshness(latestActivityAt, now),
|
|
313
317
|
degraded: summarizeDegraded(issues),
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCredentialProvenanceLabel = formatCredentialProvenanceLabel;
|
|
4
|
+
exports.formatReadyProviderLabel = formatReadyProviderLabel;
|
|
3
5
|
exports.buildFailoverContext = buildFailoverContext;
|
|
4
6
|
exports.handleFailoverReply = handleFailoverReply;
|
|
7
|
+
exports.runMachineProviderFailoverInventory = runMachineProviderFailoverInventory;
|
|
8
|
+
const identity_1 = require("./identity");
|
|
9
|
+
const provider_ping_1 = require("./provider-ping");
|
|
5
10
|
const provider_models_1 = require("./provider-models");
|
|
11
|
+
const provider_credential_pool_1 = require("./provider-credential-pool");
|
|
6
12
|
const runtime_1 = require("../nerves/runtime");
|
|
7
13
|
const CLASSIFICATION_LABELS = {
|
|
8
14
|
"auth-failure": "authentication failed",
|
|
@@ -26,6 +32,23 @@ function formatErrorDetail(errorMessage, errorSummary) {
|
|
|
26
32
|
return "";
|
|
27
33
|
return detail.length > 300 ? `${detail.slice(0, 297)}...` : detail;
|
|
28
34
|
}
|
|
35
|
+
function formatCredentialProvenanceLabel(candidate) {
|
|
36
|
+
if (candidate.contributedByAgent && candidate.source) {
|
|
37
|
+
return `credentials from ${candidate.contributedByAgent} via ${candidate.source}`;
|
|
38
|
+
}
|
|
39
|
+
if (candidate.contributedByAgent) {
|
|
40
|
+
return `credentials from ${candidate.contributedByAgent}`;
|
|
41
|
+
}
|
|
42
|
+
if (candidate.source) {
|
|
43
|
+
return `credentials from this machine via ${candidate.source}`;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function formatReadyProviderLabel(candidate) {
|
|
48
|
+
const provenance = formatCredentialProvenanceLabel(candidate);
|
|
49
|
+
const provenanceSuffix = provenance ? `; ${provenance}` : "";
|
|
50
|
+
return `${candidate.provider} (${candidate.model}${provenanceSuffix})`;
|
|
51
|
+
}
|
|
29
52
|
function formatFailingProviderLine(provider, classification, agentName) {
|
|
30
53
|
const authCommand = `ouro auth --agent ${agentName} --provider ${provider}`;
|
|
31
54
|
switch (classification) {
|
|
@@ -43,27 +66,64 @@ function formatFailingProviderLine(provider, classification, agentName) {
|
|
|
43
66
|
return ` - ${provider}: could not be reached. Run \`${authCommand}\` if credentials may be stale.`;
|
|
44
67
|
}
|
|
45
68
|
}
|
|
46
|
-
function
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const failingProviders = [];
|
|
69
|
+
function isProviderFailoverInventory(inventory) {
|
|
70
|
+
const candidate = inventory;
|
|
71
|
+
return Array.isArray(candidate.ready) && Array.isArray(candidate.unavailable) && Array.isArray(candidate.unconfigured);
|
|
72
|
+
}
|
|
73
|
+
function normalizeLegacyInventory(inventory, providerModels) {
|
|
74
|
+
const ready = [];
|
|
75
|
+
const unavailable = [];
|
|
76
|
+
const unconfigured = [];
|
|
55
77
|
for (const [provider, result] of Object.entries(inventory)) {
|
|
56
78
|
if (result.ok) {
|
|
57
|
-
|
|
79
|
+
ready.push({
|
|
80
|
+
provider,
|
|
81
|
+
model: (0, provider_models_1.resolveModelForProviderDisplay)(provider, providerModels[provider]),
|
|
82
|
+
result,
|
|
83
|
+
});
|
|
58
84
|
}
|
|
59
85
|
else if (result.classification === "auth-failure" && result.message === "no credentials configured") {
|
|
60
|
-
|
|
86
|
+
unconfigured.push(provider);
|
|
61
87
|
}
|
|
62
88
|
else {
|
|
63
89
|
// Configured but ping failed (expired token, provider also down, etc.)
|
|
64
|
-
|
|
90
|
+
unavailable.push({
|
|
91
|
+
provider,
|
|
92
|
+
model: (0, provider_models_1.resolveModelForProviderDisplay)(provider, providerModels[provider]),
|
|
93
|
+
result,
|
|
94
|
+
});
|
|
65
95
|
}
|
|
66
96
|
}
|
|
97
|
+
return { ready, unavailable, unconfigured };
|
|
98
|
+
}
|
|
99
|
+
function normalizeFailoverInventory(inventory, providerModels) {
|
|
100
|
+
if (!isProviderFailoverInventory(inventory)) {
|
|
101
|
+
return normalizeLegacyInventory(inventory, providerModels);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ready: inventory.ready.map((candidate) => ({
|
|
105
|
+
...candidate,
|
|
106
|
+
model: (0, provider_models_1.resolveModelForProviderDisplay)(candidate.provider, candidate.model),
|
|
107
|
+
})),
|
|
108
|
+
unavailable: inventory.unavailable.map((candidate) => ({
|
|
109
|
+
...candidate,
|
|
110
|
+
model: candidate.model ? (0, provider_models_1.resolveModelForProviderDisplay)(candidate.provider, candidate.model) : undefined,
|
|
111
|
+
})),
|
|
112
|
+
unconfigured: [...inventory.unconfigured],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function buildFailoverContext(errorMessage, classification, currentProvider, currentModel, agentName, inventory, providerModels, options = {}) {
|
|
116
|
+
const currentLane = options.currentLane ?? "outward";
|
|
117
|
+
const label = CLASSIFICATION_LABELS[classification];
|
|
118
|
+
const providerWithModel = formatProviderWithModel(currentProvider, currentModel);
|
|
119
|
+
const errorSummary = `${providerWithModel} ${label}`;
|
|
120
|
+
const errorDetail = formatErrorDetail(errorMessage, errorSummary);
|
|
121
|
+
const modelMismatch = (0, provider_models_1.getProviderModelMismatchMessage)(currentProvider, currentModel);
|
|
122
|
+
const normalizedInventory = normalizeFailoverInventory(inventory, providerModels);
|
|
123
|
+
const readyProviders = normalizedInventory.ready;
|
|
124
|
+
const workingProviders = readyProviders.map((candidate) => candidate.provider);
|
|
125
|
+
const unconfiguredProviders = normalizedInventory.unconfigured;
|
|
126
|
+
const failingProviders = normalizedInventory.unavailable;
|
|
67
127
|
const lines = [`${errorSummary}.`];
|
|
68
128
|
if (errorDetail) {
|
|
69
129
|
lines.push(`provider detail: ${errorDetail}`);
|
|
@@ -79,22 +139,20 @@ function buildFailoverContext(errorMessage, classification, currentProvider, cur
|
|
|
79
139
|
lines.push("Config warning:");
|
|
80
140
|
lines.push(` - ${modelMismatch}`);
|
|
81
141
|
lines.push(" - Repair the configured model with:");
|
|
82
|
-
lines.push(` \`ouro
|
|
83
|
-
lines.push(` \`ouro config model --agent ${agentName} --facing agent ${defaultModel}\``);
|
|
142
|
+
lines.push(` \`ouro use --agent ${agentName} --lane ${currentLane} --provider ${currentProvider} --model ${defaultModel}\``);
|
|
84
143
|
}
|
|
85
|
-
if (
|
|
144
|
+
if (readyProviders.length > 0) {
|
|
86
145
|
lines.push("");
|
|
87
146
|
lines.push("Ready providers:");
|
|
88
|
-
for (const
|
|
89
|
-
|
|
90
|
-
lines.push(` - ${provider} (${model}): reply "switch to ${provider}"`);
|
|
147
|
+
for (const candidate of readyProviders) {
|
|
148
|
+
lines.push(` - ${formatReadyProviderLabel(candidate)}: reply "switch to ${candidate.provider}"`);
|
|
91
149
|
}
|
|
92
150
|
}
|
|
93
151
|
if (failingProviders.length > 0) {
|
|
94
152
|
lines.push("");
|
|
95
153
|
lines.push("Configured but unavailable:");
|
|
96
|
-
for (const
|
|
97
|
-
lines.push(formatFailingProviderLine(provider, classification, agentName));
|
|
154
|
+
for (const candidate of failingProviders) {
|
|
155
|
+
lines.push(formatFailingProviderLine(candidate.provider, candidate.result.classification, agentName));
|
|
98
156
|
}
|
|
99
157
|
}
|
|
100
158
|
if (unconfiguredProviders.length > 0) {
|
|
@@ -112,24 +170,105 @@ function buildFailoverContext(errorMessage, classification, currentProvider, cur
|
|
|
112
170
|
component: "engine",
|
|
113
171
|
event: "engine.failover_context_built",
|
|
114
172
|
message: "built provider failover context",
|
|
115
|
-
meta: { currentProvider, classification, workingProviders, unconfiguredProviders },
|
|
173
|
+
meta: { currentProvider, currentLane, classification, workingProviders, unconfiguredProviders },
|
|
116
174
|
});
|
|
117
175
|
return {
|
|
118
176
|
errorSummary,
|
|
119
177
|
classification,
|
|
120
178
|
currentProvider,
|
|
179
|
+
currentLane,
|
|
121
180
|
agentName,
|
|
122
181
|
workingProviders,
|
|
182
|
+
readyProviders,
|
|
123
183
|
unconfiguredProviders,
|
|
124
184
|
userMessage: lines.join("\n"),
|
|
125
185
|
};
|
|
126
186
|
}
|
|
127
187
|
function handleFailoverReply(reply, context) {
|
|
128
188
|
const lower = reply.toLowerCase().trim();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
189
|
+
const readyProviders = context.readyProviders ?? context.workingProviders.map((provider) => ({
|
|
190
|
+
provider,
|
|
191
|
+
model: (0, provider_models_1.resolveModelForProviderDisplay)(provider),
|
|
192
|
+
}));
|
|
193
|
+
const currentLane = context.currentLane ?? "outward";
|
|
194
|
+
for (const candidate of readyProviders) {
|
|
195
|
+
if (lower.includes(`switch to ${candidate.provider}`) || lower === candidate.provider) {
|
|
196
|
+
return {
|
|
197
|
+
action: "switch",
|
|
198
|
+
provider: candidate.provider,
|
|
199
|
+
model: candidate.model,
|
|
200
|
+
lane: currentLane,
|
|
201
|
+
...(candidate.credentialRevision ? { credentialRevision: candidate.credentialRevision } : {}),
|
|
202
|
+
...(candidate.source ? { source: candidate.source } : {}),
|
|
203
|
+
...(candidate.contributedByAgent ? { contributedByAgent: candidate.contributedByAgent } : {}),
|
|
204
|
+
};
|
|
132
205
|
}
|
|
133
206
|
}
|
|
134
207
|
return { action: "dismiss" };
|
|
135
208
|
}
|
|
209
|
+
function candidateFromCredentialRecord(record) {
|
|
210
|
+
return {
|
|
211
|
+
provider: record.provider,
|
|
212
|
+
credentialRevision: record.revision,
|
|
213
|
+
source: record.provenance.source,
|
|
214
|
+
contributedByAgent: record.provenance.contributedByAgent,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async function runMachineProviderFailoverInventory(agentName, currentProvider, options = {}) {
|
|
218
|
+
const ping = options.ping ?? provider_ping_1.pingProvider;
|
|
219
|
+
const poolResult = (0, provider_credential_pool_1.readProviderCredentialPool)(options.homeDir);
|
|
220
|
+
const providers = Object.keys(identity_1.PROVIDER_CREDENTIALS).filter((provider) => provider !== currentProvider);
|
|
221
|
+
const inventory = { ready: [], unavailable: [], unconfigured: [] };
|
|
222
|
+
if (!poolResult.ok) {
|
|
223
|
+
inventory.unconfigured.push(...providers);
|
|
224
|
+
(0, runtime_1.emitNervesEvent)({
|
|
225
|
+
component: "engine",
|
|
226
|
+
event: "engine.machine_failover_inventory_built",
|
|
227
|
+
message: "built machine provider failover inventory",
|
|
228
|
+
meta: { agentName, currentProvider, credentialPoolStatus: poolResult.reason, readyCount: 0, unavailableCount: 0, unconfiguredCount: inventory.unconfigured.length },
|
|
229
|
+
});
|
|
230
|
+
return inventory;
|
|
231
|
+
}
|
|
232
|
+
const results = await Promise.all(providers.map(async (provider) => {
|
|
233
|
+
const record = poolResult.pool.providers[provider];
|
|
234
|
+
if (!record)
|
|
235
|
+
return { provider, record: undefined, result: undefined };
|
|
236
|
+
const model = (0, provider_models_1.getDefaultModelForProvider)(provider);
|
|
237
|
+
const config = { ...record.credentials, ...record.config };
|
|
238
|
+
const result = await ping(provider, config, { model });
|
|
239
|
+
return { provider, record, model, result };
|
|
240
|
+
}));
|
|
241
|
+
for (const entry of results) {
|
|
242
|
+
if (!entry.record) {
|
|
243
|
+
inventory.unconfigured.push(entry.provider);
|
|
244
|
+
}
|
|
245
|
+
else if (entry.result.ok) {
|
|
246
|
+
inventory.ready.push({
|
|
247
|
+
...candidateFromCredentialRecord(entry.record),
|
|
248
|
+
model: entry.model,
|
|
249
|
+
result: entry.result,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
inventory.unavailable.push({
|
|
254
|
+
...candidateFromCredentialRecord(entry.record),
|
|
255
|
+
model: entry.model,
|
|
256
|
+
result: entry.result,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
(0, runtime_1.emitNervesEvent)({
|
|
261
|
+
component: "engine",
|
|
262
|
+
event: "engine.machine_failover_inventory_built",
|
|
263
|
+
message: "built machine provider failover inventory",
|
|
264
|
+
meta: {
|
|
265
|
+
agentName,
|
|
266
|
+
currentProvider,
|
|
267
|
+
credentialPoolStatus: "present",
|
|
268
|
+
readyCount: inventory.ready.length,
|
|
269
|
+
unavailableCount: inventory.unavailable.length,
|
|
270
|
+
unconfiguredCount: inventory.unconfigured.length,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
return inventory;
|
|
274
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildAgentProviderVisibility = buildAgentProviderVisibility;
|
|
4
|
+
exports.formatProviderVisibilityLine = formatProviderVisibilityLine;
|
|
5
|
+
exports.formatAgentProviderVisibilityForPrompt = formatAgentProviderVisibilityForPrompt;
|
|
6
|
+
exports.formatAgentProviderVisibilityForStartOfTurn = formatAgentProviderVisibilityForStartOfTurn;
|
|
7
|
+
exports.formatAgentProviderVisibilityForPulse = formatAgentProviderVisibilityForPulse;
|
|
8
|
+
exports.providerVisibilityStatusRows = providerVisibilityStatusRows;
|
|
9
|
+
exports.isAgentProviderVisibility = isAgentProviderVisibility;
|
|
10
|
+
const runtime_1 = require("../nerves/runtime");
|
|
11
|
+
const provider_binding_resolver_1 = require("./provider-binding-resolver");
|
|
12
|
+
const LANES = ["outward", "inner"];
|
|
13
|
+
function credentialVisibility(binding) {
|
|
14
|
+
const credential = binding.credential;
|
|
15
|
+
if (credential.status === "present") {
|
|
16
|
+
return {
|
|
17
|
+
status: "present",
|
|
18
|
+
source: credential.source,
|
|
19
|
+
contributedByAgent: credential.contributedByAgent,
|
|
20
|
+
revision: credential.revision,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
status: credential.status,
|
|
25
|
+
repairCommand: credential.repair.command,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function readinessVisibility(binding) {
|
|
29
|
+
return {
|
|
30
|
+
status: binding.readiness.status,
|
|
31
|
+
...(binding.readiness.checkedAt ? { checkedAt: binding.readiness.checkedAt } : {}),
|
|
32
|
+
...(binding.readiness.error ? { error: binding.readiness.error } : {}),
|
|
33
|
+
...(binding.readiness.reason ? { reason: binding.readiness.reason } : {}),
|
|
34
|
+
...(binding.readiness.attempts !== undefined ? { attempts: binding.readiness.attempts } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function visibilityForLane(input, lane) {
|
|
38
|
+
const resolved = (0, provider_binding_resolver_1.resolveEffectiveProviderBinding)({ ...input, lane });
|
|
39
|
+
if (!resolved.ok) {
|
|
40
|
+
return {
|
|
41
|
+
lane,
|
|
42
|
+
status: "unconfigured",
|
|
43
|
+
provider: "unconfigured",
|
|
44
|
+
model: "-",
|
|
45
|
+
source: "missing",
|
|
46
|
+
readiness: {
|
|
47
|
+
status: "unknown",
|
|
48
|
+
reason: resolved.reason,
|
|
49
|
+
},
|
|
50
|
+
credential: {
|
|
51
|
+
status: "missing",
|
|
52
|
+
repairCommand: resolved.repair.command,
|
|
53
|
+
},
|
|
54
|
+
repairCommand: resolved.repair.command,
|
|
55
|
+
reason: resolved.reason,
|
|
56
|
+
warnings: resolved.warnings.map((warning) => warning.message),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
lane,
|
|
61
|
+
status: "configured",
|
|
62
|
+
provider: resolved.binding.provider,
|
|
63
|
+
model: resolved.binding.model,
|
|
64
|
+
source: resolved.binding.source,
|
|
65
|
+
readiness: readinessVisibility(resolved.binding),
|
|
66
|
+
credential: credentialVisibility(resolved.binding),
|
|
67
|
+
warnings: resolved.binding.warnings.map((warning) => warning.message),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function buildAgentProviderVisibility(input) {
|
|
71
|
+
const visibility = {
|
|
72
|
+
agentName: input.agentName,
|
|
73
|
+
lanes: LANES.map((lane) => visibilityForLane(input, lane)),
|
|
74
|
+
};
|
|
75
|
+
(0, runtime_1.emitNervesEvent)({
|
|
76
|
+
component: "config/identity",
|
|
77
|
+
event: "config.provider_visibility_built",
|
|
78
|
+
message: "built provider visibility summary",
|
|
79
|
+
meta: {
|
|
80
|
+
agentName: input.agentName,
|
|
81
|
+
laneStatuses: visibility.lanes.map((lane) => `${lane.lane}:${lane.status}`).join(","),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
return visibility;
|
|
85
|
+
}
|
|
86
|
+
function credentialLabel(credential) {
|
|
87
|
+
if (credential.status === "present") {
|
|
88
|
+
const source = credential.source ?? "unknown";
|
|
89
|
+
return credential.contributedByAgent ? `${source} from ${credential.contributedByAgent}` : source;
|
|
90
|
+
}
|
|
91
|
+
if (credential.status === "invalid-pool")
|
|
92
|
+
return "invalid pool";
|
|
93
|
+
return "missing";
|
|
94
|
+
}
|
|
95
|
+
function readinessLabel(readiness) {
|
|
96
|
+
if (readiness.status === "failed") {
|
|
97
|
+
return readiness.error ? `failed: ${readiness.error}` : "failed";
|
|
98
|
+
}
|
|
99
|
+
if (readiness.status === "stale") {
|
|
100
|
+
return readiness.reason ? `stale: ${readiness.reason}` : "stale";
|
|
101
|
+
}
|
|
102
|
+
if (readiness.status === "unknown") {
|
|
103
|
+
return readiness.reason ? `unknown: ${readiness.reason}` : "unknown";
|
|
104
|
+
}
|
|
105
|
+
return readiness.status;
|
|
106
|
+
}
|
|
107
|
+
function formatProviderVisibilityLine(lane) {
|
|
108
|
+
if (lane.status === "unconfigured") {
|
|
109
|
+
return `${lane.lane}: unconfigured (${lane.reason}); repair: ${lane.repairCommand}`;
|
|
110
|
+
}
|
|
111
|
+
const parts = [
|
|
112
|
+
readinessLabel(lane.readiness),
|
|
113
|
+
`source: ${lane.source}`,
|
|
114
|
+
`credentials: ${credentialLabel(lane.credential)}`,
|
|
115
|
+
];
|
|
116
|
+
if (lane.credential.revision)
|
|
117
|
+
parts.push(`revision: ${lane.credential.revision}`);
|
|
118
|
+
if (lane.credential.repairCommand)
|
|
119
|
+
parts.push(`repair: ${lane.credential.repairCommand}`);
|
|
120
|
+
if (lane.warnings.length > 0)
|
|
121
|
+
parts.push(`warnings: ${lane.warnings.join("; ")}`);
|
|
122
|
+
return `${lane.lane}: ${lane.provider} / ${lane.model} [${parts.join("; ")}]`;
|
|
123
|
+
}
|
|
124
|
+
function formatAgentProviderVisibilityForPrompt(visibility) {
|
|
125
|
+
if (visibility.lanes.every((lane) => lane.status === "unconfigured")) {
|
|
126
|
+
return [
|
|
127
|
+
"provider bindings are not configured on this machine.",
|
|
128
|
+
...visibility.lanes.map((lane) => `- ${formatProviderVisibilityLine(lane)}`),
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
"runtime uses local provider bindings for this machine:",
|
|
133
|
+
...visibility.lanes.map((lane) => `- ${formatProviderVisibilityLine(lane)}`),
|
|
134
|
+
].join("\n");
|
|
135
|
+
}
|
|
136
|
+
function formatAgentProviderVisibilityForStartOfTurn(visibility) {
|
|
137
|
+
return visibility.lanes.map((lane) => `- ${formatProviderVisibilityLine(lane)}`).join("\n");
|
|
138
|
+
}
|
|
139
|
+
function formatAgentProviderVisibilityForPulse(visibility) {
|
|
140
|
+
return visibility.lanes.map((lane) => formatProviderVisibilityLine(lane)).join("; ");
|
|
141
|
+
}
|
|
142
|
+
function providerVisibilityStatusRows(visibility) {
|
|
143
|
+
return visibility.lanes.map((lane) => {
|
|
144
|
+
if (lane.status === "unconfigured") {
|
|
145
|
+
return {
|
|
146
|
+
agent: visibility.agentName,
|
|
147
|
+
lane: lane.lane,
|
|
148
|
+
provider: "unconfigured",
|
|
149
|
+
model: "-",
|
|
150
|
+
source: "missing",
|
|
151
|
+
readiness: "unknown",
|
|
152
|
+
detail: lane.repairCommand,
|
|
153
|
+
credential: "missing",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
agent: visibility.agentName,
|
|
158
|
+
lane: lane.lane,
|
|
159
|
+
provider: lane.provider,
|
|
160
|
+
model: lane.model,
|
|
161
|
+
source: lane.source,
|
|
162
|
+
readiness: lane.readiness.status,
|
|
163
|
+
...(lane.readiness.error ? { detail: lane.readiness.error } : {}),
|
|
164
|
+
credential: credentialLabel(lane.credential),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function isAgentProviderVisibility(value) {
|
|
169
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
170
|
+
return false;
|
|
171
|
+
const record = value;
|
|
172
|
+
if (typeof record.agentName !== "string")
|
|
173
|
+
return false;
|
|
174
|
+
if (!Array.isArray(record.lanes))
|
|
175
|
+
return false;
|
|
176
|
+
return record.lanes.every((lane) => {
|
|
177
|
+
if (!lane || typeof lane !== "object" || Array.isArray(lane))
|
|
178
|
+
return false;
|
|
179
|
+
const laneRecord = lane;
|
|
180
|
+
return (laneRecord.lane === "outward" || laneRecord.lane === "inner")
|
|
181
|
+
&& (laneRecord.status === "configured" || laneRecord.status === "unconfigured");
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -228,6 +228,7 @@ function renderStartOfTurnPacket(packet) {
|
|
|
228
228
|
// are actionable "fix your git" signals. bundleState is preferred
|
|
229
229
|
// because it's structured (array of enum values) while syncFailure is
|
|
230
230
|
// a legacy free-form string; both render when populated.
|
|
231
|
+
{ label: "provider", content: packet.providerState ?? "", priority: 8 },
|
|
231
232
|
{ label: "bundleState", content: (0, bundle_state_1.renderBundleStateHint)(packet.bundleState ?? []), priority: 7 },
|
|
232
233
|
{ label: "syncFailure", content: packet.syncFailure ?? "", priority: 7 },
|
|
233
234
|
{ label: "resume", content: packet.resumeHint, priority: 6 },
|
|
@@ -305,6 +306,9 @@ function formatSections(sections) {
|
|
|
305
306
|
case "bundleState":
|
|
306
307
|
parts.push(`**Bundle:** ${section.content}`);
|
|
307
308
|
break;
|
|
309
|
+
case "provider":
|
|
310
|
+
parts.push(`**Provider:**\n${section.content}`);
|
|
311
|
+
break;
|
|
308
312
|
}
|
|
309
313
|
}
|
|
310
314
|
return parts.join("\n\n");
|
|
@@ -59,6 +59,7 @@ const cares_1 = require("../arc/cares");
|
|
|
59
59
|
const config_1 = require("./config");
|
|
60
60
|
const daemon_health_1 = require("./daemon/daemon-health");
|
|
61
61
|
const prompt_1 = require("../mind/prompt");
|
|
62
|
+
const provider_visibility_1 = require("./provider-visibility");
|
|
62
63
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
63
64
|
const DAEMON_SOCKET_PATH = "/tmp/ouroboros-daemon.sock";
|
|
64
65
|
function isLiveCodingSessionStatus(status) {
|
|
@@ -306,6 +307,7 @@ async function buildTurnContext(input) {
|
|
|
306
307
|
/* v8 ignore stop */
|
|
307
308
|
}
|
|
308
309
|
const bundleMeta = readBundleMetaFile();
|
|
310
|
+
const providerVisibility = (0, provider_visibility_1.buildAgentProviderVisibility)({ agentName, agentRoot });
|
|
309
311
|
let daemonHealth = null;
|
|
310
312
|
try {
|
|
311
313
|
daemonHealth = (0, daemon_health_1.readHealth)((0, daemon_health_1.getDefaultHealthPath)());
|
|
@@ -333,6 +335,7 @@ async function buildTurnContext(input) {
|
|
|
333
335
|
bridgeCount: activeBridges.length,
|
|
334
336
|
codingSessionCount: codingSessions.length,
|
|
335
337
|
episodeCount: recentEpisodes.length,
|
|
338
|
+
providerLaneCount: providerVisibility.lanes.length,
|
|
336
339
|
},
|
|
337
340
|
});
|
|
338
341
|
return {
|
|
@@ -349,6 +352,7 @@ async function buildTurnContext(input) {
|
|
|
349
352
|
activeCares,
|
|
350
353
|
syncConfig,
|
|
351
354
|
syncFailure: undefined, // Set by pipeline after preTurnPull
|
|
355
|
+
providerVisibility,
|
|
352
356
|
daemonRunning,
|
|
353
357
|
senseStatusLines,
|
|
354
358
|
bundleMeta,
|
package/dist/mind/prompt.js
CHANGED
|
@@ -78,6 +78,7 @@ const obligation_steering_1 = require("./obligation-steering");
|
|
|
78
78
|
const daemon_health_1 = require("../heart/daemon/daemon-health");
|
|
79
79
|
const scrutiny_1 = require("./scrutiny");
|
|
80
80
|
const pulse_1 = require("../heart/daemon/pulse");
|
|
81
|
+
const provider_visibility_1 = require("../heart/provider-visibility");
|
|
81
82
|
// Lazy-loaded psyche text cache
|
|
82
83
|
let _psycheCache = null;
|
|
83
84
|
let _senseStatusLinesCache = null;
|
|
@@ -459,7 +460,10 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
|
|
|
459
460
|
}
|
|
460
461
|
return lines;
|
|
461
462
|
}
|
|
462
|
-
function providerSection(channel) {
|
|
463
|
+
function providerSection(channel, options) {
|
|
464
|
+
if (options?.providerVisibility) {
|
|
465
|
+
return `## my provider\n${(0, provider_visibility_1.formatAgentProviderVisibilityForPrompt)(options.providerVisibility)}`;
|
|
466
|
+
}
|
|
463
467
|
return `## my provider\n${(0, core_1.getProviderDisplayLabel)((0, channel_1.channelToFacing)(channel))}`;
|
|
464
468
|
}
|
|
465
469
|
function dateSection() {
|
|
@@ -727,6 +731,8 @@ function pulseSection(channel = "cli") {
|
|
|
727
731
|
if (sib.fixHint)
|
|
728
732
|
lines.push(` fix: ${sib.fixHint}`);
|
|
729
733
|
lines.push(` bundle: \`${sib.bundlePath}\``);
|
|
734
|
+
if (sib.providerVisibility)
|
|
735
|
+
lines.push(` provider: ${(0, provider_visibility_1.formatAgentProviderVisibilityForPulse)(sib.providerVisibility)}`);
|
|
730
736
|
}
|
|
731
737
|
lines.push("");
|
|
732
738
|
}
|
|
@@ -737,6 +743,8 @@ function pulseSection(channel = "cli") {
|
|
|
737
743
|
for (const sib of healthy) {
|
|
738
744
|
const activity = sib.currentActivity ? ` — ${sib.currentActivity}` : "";
|
|
739
745
|
lines.push(`- **${sib.name}** is running${activity}. bundle: \`${sib.bundlePath}\``);
|
|
746
|
+
if (sib.providerVisibility)
|
|
747
|
+
lines.push(` provider: ${(0, provider_visibility_1.formatAgentProviderVisibilityForPulse)(sib.providerVisibility)}`);
|
|
740
748
|
}
|
|
741
749
|
lines.push("");
|
|
742
750
|
}
|
|
@@ -744,6 +752,8 @@ function pulseSection(channel = "cli") {
|
|
|
744
752
|
lines.push("**idle siblings** — configured but not currently running:");
|
|
745
753
|
for (const sib of idle) {
|
|
746
754
|
lines.push(`- **${sib.name}** (status: ${sib.status}). bundle: \`${sib.bundlePath}\``);
|
|
755
|
+
if (sib.providerVisibility)
|
|
756
|
+
lines.push(` provider: ${(0, provider_visibility_1.formatAgentProviderVisibilityForPulse)(sib.providerVisibility)}`);
|
|
747
757
|
}
|
|
748
758
|
lines.push("");
|
|
749
759
|
}
|
|
@@ -1260,7 +1270,7 @@ async function buildSystem(channel = "cli", options, context) {
|
|
|
1260
1270
|
runtimeInfoSection(channel, options),
|
|
1261
1271
|
rhythmStatusSection(options?.daemonHealth),
|
|
1262
1272
|
channelNatureSection((0, channel_1.getChannelCapabilities)(channel)),
|
|
1263
|
-
providerSection(channel),
|
|
1273
|
+
providerSection(channel, options),
|
|
1264
1274
|
dateSection(),
|
|
1265
1275
|
// Group 3: my tools & capabilities
|
|
1266
1276
|
"# my tools & capabilities",
|
package/dist/senses/pipeline.js
CHANGED
|
@@ -52,9 +52,7 @@ const active_work_1 = require("../heart/active-work");
|
|
|
52
52
|
const delegation_1 = require("../heart/delegation");
|
|
53
53
|
const obligations_1 = require("../arc/obligations");
|
|
54
54
|
const provider_failover_1 = require("../heart/provider-failover");
|
|
55
|
-
const
|
|
56
|
-
const auth_flow_1 = require("../heart/auth/auth-flow");
|
|
57
|
-
const provider_models_1 = require("../heart/provider-models");
|
|
55
|
+
const provider_state_1 = require("../heart/provider-state");
|
|
58
56
|
const tempo_1 = require("../heart/tempo");
|
|
59
57
|
const temporal_view_1 = require("../heart/temporal-view");
|
|
60
58
|
const start_of_turn_packet_1 = require("../heart/start-of-turn-packet");
|
|
@@ -65,6 +63,7 @@ const session_events_1 = require("../heart/session-events");
|
|
|
65
63
|
const presence_1 = require("../arc/presence");
|
|
66
64
|
const episodes_1 = require("../arc/episodes");
|
|
67
65
|
const turn_context_1 = require("../heart/turn-context");
|
|
66
|
+
const provider_visibility_1 = require("../heart/provider-visibility");
|
|
68
67
|
/**
|
|
69
68
|
* Emit episodes for obligation state transitions detected during a turn.
|
|
70
69
|
* Exported for direct testability (avoids v8 coverage merge issues in multi-file test suites).
|
|
@@ -85,6 +84,52 @@ function emitObligationTransitionEpisodes(agentRoot, preTurnObligationIds, postT
|
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
}
|
|
87
|
+
function providerLaneForChannel(channel) {
|
|
88
|
+
return channel === "inner" ? "inner" : "outward";
|
|
89
|
+
}
|
|
90
|
+
function resolveCurrentFailoverBinding(agentName, lane) {
|
|
91
|
+
const stateResult = (0, provider_state_1.readProviderState)((0, identity_1.getAgentRoot)(agentName));
|
|
92
|
+
if (stateResult.ok) {
|
|
93
|
+
const binding = stateResult.state.lanes[lane];
|
|
94
|
+
return { provider: binding.provider, model: binding.model };
|
|
95
|
+
}
|
|
96
|
+
const agentConfig = (0, identity_1.loadAgentConfig)();
|
|
97
|
+
const fallback = lane === "inner" ? agentConfig.agentFacing : agentConfig.humanFacing;
|
|
98
|
+
return { provider: fallback.provider, model: fallback.model };
|
|
99
|
+
}
|
|
100
|
+
function writeFailoverProviderStateSwitch(agentName, action) {
|
|
101
|
+
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
102
|
+
const stateResult = (0, provider_state_1.readProviderState)(agentRoot);
|
|
103
|
+
if (!stateResult.ok) {
|
|
104
|
+
throw new Error(`Cannot switch ${action.lane} lane for ${agentName}: ${stateResult.error}`);
|
|
105
|
+
}
|
|
106
|
+
const updatedAt = new Date().toISOString();
|
|
107
|
+
const lanes = { ...stateResult.state.lanes };
|
|
108
|
+
lanes[action.lane] = {
|
|
109
|
+
provider: action.provider,
|
|
110
|
+
model: action.model,
|
|
111
|
+
source: "local",
|
|
112
|
+
updatedAt,
|
|
113
|
+
};
|
|
114
|
+
const readiness = { ...stateResult.state.readiness };
|
|
115
|
+
readiness[action.lane] = {
|
|
116
|
+
status: "ready",
|
|
117
|
+
provider: action.provider,
|
|
118
|
+
model: action.model,
|
|
119
|
+
checkedAt: updatedAt,
|
|
120
|
+
...(action.credentialRevision ? { credentialRevision: action.credentialRevision } : {}),
|
|
121
|
+
};
|
|
122
|
+
(0, provider_state_1.writeProviderState)(agentRoot, {
|
|
123
|
+
...stateResult.state,
|
|
124
|
+
updatedAt,
|
|
125
|
+
lanes,
|
|
126
|
+
readiness,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function formatFailoverSwitchLabel(action) {
|
|
130
|
+
const provenance = (0, provider_failover_1.formatCredentialProvenanceLabel)(action);
|
|
131
|
+
return `${action.provider} (${action.model}${provenance ? `; ${provenance}` : ""})`;
|
|
132
|
+
}
|
|
88
133
|
function prependTurnSections(message, sections) {
|
|
89
134
|
/* v8 ignore next -- defensive: only user messages with non-empty sections reach here @preserve */
|
|
90
135
|
if (message.role !== "user" || sections.length === 0)
|
|
@@ -133,8 +178,7 @@ async function handleInboundTurn(input) {
|
|
|
133
178
|
if (failoverAction.action === "switch") {
|
|
134
179
|
let switchSucceeded = false;
|
|
135
180
|
try {
|
|
136
|
-
(
|
|
137
|
-
(0, auth_flow_1.writeAgentProviderSelection)(failoverAgentName, "agent", failoverAction.provider);
|
|
181
|
+
writeFailoverProviderStateSwitch(failoverAgentName, failoverAction);
|
|
138
182
|
switchSucceeded = true;
|
|
139
183
|
/* v8 ignore start -- defensive: write failure during provider switch @preserve */
|
|
140
184
|
}
|
|
@@ -143,8 +187,8 @@ async function handleInboundTurn(input) {
|
|
|
143
187
|
level: "error",
|
|
144
188
|
component: "senses",
|
|
145
189
|
event: "senses.failover_switch_error",
|
|
146
|
-
message: `failed to switch provider to ${failoverAction.provider}`,
|
|
147
|
-
meta: { agentName: failoverAgentName, provider: failoverAction.provider, error: switchError instanceof Error ? switchError.message : String(switchError) },
|
|
190
|
+
message: `failed to switch ${failoverAction.lane} provider lane to ${failoverAction.provider}`,
|
|
191
|
+
meta: { agentName: failoverAgentName, lane: failoverAction.lane, provider: failoverAction.provider, model: failoverAction.model, error: switchError instanceof Error ? switchError.message : String(switchError) },
|
|
148
192
|
});
|
|
149
193
|
}
|
|
150
194
|
/* v8 ignore stop */
|
|
@@ -153,28 +197,24 @@ async function handleInboundTurn(input) {
|
|
|
153
197
|
(0, runtime_1.emitNervesEvent)({
|
|
154
198
|
component: "senses",
|
|
155
199
|
event: "senses.failover_switch",
|
|
156
|
-
message: `switched provider to ${failoverAction.provider} via failover`,
|
|
157
|
-
meta: {
|
|
200
|
+
message: `switched ${failoverAction.lane} provider lane to ${failoverAction.provider} via failover`,
|
|
201
|
+
meta: {
|
|
202
|
+
agentName: failoverAgentName,
|
|
203
|
+
lane: failoverAction.lane,
|
|
204
|
+
provider: failoverAction.provider,
|
|
205
|
+
model: failoverAction.model,
|
|
206
|
+
credentialRevision: failoverAction.credentialRevision,
|
|
207
|
+
source: failoverAction.source,
|
|
208
|
+
contributedByAgent: failoverAction.contributedByAgent,
|
|
209
|
+
},
|
|
158
210
|
});
|
|
159
211
|
// Replace "switch to <provider>" with a context message for the agent.
|
|
160
212
|
// The session already has the user's original question from the failed turn.
|
|
161
213
|
// The agent needs to know what happened so it can respond appropriately.
|
|
162
|
-
const
|
|
163
|
-
try {
|
|
164
|
-
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
|
|
165
|
-
const cfg = secrets.providers[failoverAction.provider];
|
|
166
|
-
const hint = cfg?.model ?? cfg?.modelName;
|
|
167
|
-
return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider, typeof hint === "string" ? hint : "");
|
|
168
|
-
/* v8 ignore next 2 -- defensive: secrets read failure @preserve */
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider);
|
|
172
|
-
}
|
|
173
|
-
})();
|
|
174
|
-
const newProviderLabel = `${failoverAction.provider} (${newProviderSecrets})`;
|
|
214
|
+
const newProviderLabel = formatFailoverSwitchLabel(failoverAction);
|
|
175
215
|
input.messages = [{
|
|
176
216
|
role: "user",
|
|
177
|
-
content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
|
|
217
|
+
content: `[provider switch: ${pendingContext.errorSummary}. switched ${failoverAction.lane} lane to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
|
|
178
218
|
}];
|
|
179
219
|
input.switchedProvider = failoverAction.provider;
|
|
180
220
|
}
|
|
@@ -396,6 +436,9 @@ async function handleInboundTurn(input) {
|
|
|
396
436
|
if (syncFailure) {
|
|
397
437
|
startOfTurnPacket.syncFailure = syncFailure;
|
|
398
438
|
}
|
|
439
|
+
if (ctx.providerVisibility) {
|
|
440
|
+
startOfTurnPacket.providerState = (0, provider_visibility_1.formatAgentProviderVisibilityForStartOfTurn)(ctx.providerVisibility);
|
|
441
|
+
}
|
|
399
442
|
// Structured bundle state detection — surfaces discrete issues the
|
|
400
443
|
// agent can remediate via the bundle_* tools. Runs independently of
|
|
401
444
|
// syncFailure so the two signals coexist during the transition away
|
|
@@ -450,6 +493,7 @@ async function handleInboundTurn(input) {
|
|
|
450
493
|
bundleMeta: ctx.bundleMeta,
|
|
451
494
|
daemonHealth: ctx.daemonHealth,
|
|
452
495
|
journalFiles: ctx.journalFiles,
|
|
496
|
+
...(ctx.providerVisibility ? { providerVisibility: ctx.providerVisibility } : {}),
|
|
453
497
|
toolContext: {
|
|
454
498
|
/* v8 ignore next -- default no-op signin satisfies interface; real signin injected by sense adapter @preserve */
|
|
455
499
|
signin: async () => undefined,
|
|
@@ -465,23 +509,15 @@ async function handleInboundTurn(input) {
|
|
|
465
509
|
if (result.outcome === "errored" && input.failoverState) {
|
|
466
510
|
try {
|
|
467
511
|
const agentName = (0, identity_1.getAgentName)();
|
|
468
|
-
const
|
|
469
|
-
const
|
|
512
|
+
const currentLane = providerLaneForChannel(input.channel);
|
|
513
|
+
const currentBinding = resolveCurrentFailoverBinding(agentName, currentLane);
|
|
514
|
+
const currentProvider = currentBinding.provider;
|
|
470
515
|
/* v8 ignore next -- defensive: errorClassification always set when errored @preserve */
|
|
471
516
|
const classification = result.errorClassification ?? "unknown";
|
|
472
|
-
const inventory = await (0,
|
|
473
|
-
const { secrets } = (0, auth_flow_1.loadAgentSecrets)(agentName);
|
|
474
|
-
const providerModels = {};
|
|
475
|
-
for (const [p, cfg] of Object.entries(secrets.providers)) {
|
|
476
|
-
const model = cfg.model ?? cfg.modelName;
|
|
477
|
-
if (typeof model === "string" && model)
|
|
478
|
-
providerModels[p] = model;
|
|
479
|
-
}
|
|
480
|
-
// Use agent.json model (source of truth), not secrets model (may be stale)
|
|
481
|
-
const currentModel = agentConfig.humanFacing.model;
|
|
517
|
+
const inventory = await (0, provider_failover_1.runMachineProviderFailoverInventory)(agentName, currentProvider);
|
|
482
518
|
const failoverContext = (0, provider_failover_1.buildFailoverContext)(
|
|
483
519
|
/* v8 ignore next -- defensive: error always set when errored @preserve */
|
|
484
|
-
result.error?.message ?? "unknown error", classification, currentProvider,
|
|
520
|
+
result.error?.message ?? "unknown error", classification, currentProvider, currentBinding.model, agentName, inventory, {}, { currentLane });
|
|
485
521
|
input.failoverState.pending = failoverContext;
|
|
486
522
|
input.postTurn(sessionMessages, session.sessionPath, result.usage);
|
|
487
523
|
return {
|