paseo-bm-plugin 0.0.0-placeholder.0
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/LICENSE +21 -0
- package/README.md +53 -0
- package/client/agent-tree.ts +308 -0
- package/client/answer-state.ts +62 -0
- package/client/bead-chips.tsx +147 -0
- package/client/beads-header-button.ts +108 -0
- package/client/beads-model.ts +581 -0
- package/client/beads-screen.tsx +516 -0
- package/client/beads-tab.tsx +58 -0
- package/client/chat-card.tsx +636 -0
- package/client/chat-cards.ts +1038 -0
- package/client/dashboard-actions.tsx +255 -0
- package/client/dashboard-model.ts +947 -0
- package/client/dashboard-view.ts +215 -0
- package/client/dashboard.tsx +318 -0
- package/client/launch-manager.ts +323 -0
- package/client/launcher.tsx +516 -0
- package/client/markdown-view.tsx +112 -0
- package/client/markdown.ts +145 -0
- package/client/settings.tsx +104 -0
- package/client/setup-model.ts +552 -0
- package/client/setup-screen.tsx +913 -0
- package/client/slot.ts +47 -0
- package/client/tree.tsx +204 -0
- package/client/ui.tsx +262 -0
- package/client/waiting-pills-model.ts +156 -0
- package/client/waiting-pills.tsx +201 -0
- package/index.client.tsx +232 -0
- package/index.server.ts +168 -0
- package/package.json +35 -0
- package/paseo-plugin.json +6 -0
- package/roles/manager.md +181 -0
- package/roles/reviewer.md +160 -0
- package/roles/worker.md +407 -0
- package/server/agent-labels.ts +194 -0
- package/server/agent-role.ts +102 -0
- package/server/answer-marks.ts +120 -0
- package/server/bead-actions.ts +88 -0
- package/server/bead-work.ts +80 -0
- package/server/beads-store.ts +342 -0
- package/server/bm-report.ts +433 -0
- package/server/chat-peers.ts +65 -0
- package/server/chat-rpc.ts +122 -0
- package/server/chat-waiting.ts +182 -0
- package/server/collector.ts +629 -0
- package/server/config-writer.ts +222 -0
- package/server/cost.ts +88 -0
- package/server/dashboard-rpc.ts +662 -0
- package/server/fallback-detect.ts +183 -0
- package/server/fallback-handover.ts +365 -0
- package/server/fallback-manager.ts +170 -0
- package/server/fallback-reviewer.ts +198 -0
- package/server/fallback-rpc.ts +306 -0
- package/server/fallback-settings.ts +322 -0
- package/server/fallback-state.ts +518 -0
- package/server/fallback-switch.ts +191 -0
- package/server/fallback-wait.ts +188 -0
- package/server/format-check.ts +352 -0
- package/server/install-home.ts +187 -0
- package/server/live-timeline.ts +129 -0
- package/server/manager-instructions.ts +9 -0
- package/server/manager.ts +647 -0
- package/server/model-costs.ts +238 -0
- package/server/notice-queue.ts +315 -0
- package/server/notices.ts +81 -0
- package/server/paseo-cli.ts +115 -0
- package/server/provider-id.ts +12 -0
- package/server/review-budget.ts +208 -0
- package/server/reviewer-instructions.ts +9 -0
- package/server/role-choices.ts +161 -0
- package/server/role-extras.ts +270 -0
- package/server/role-hook.ts +347 -0
- package/server/role-mode.ts +397 -0
- package/server/role-settings-rpc.ts +325 -0
- package/server/roles.ts +96 -0
- package/server/settings-notices.ts +112 -0
- package/server/setup-rpc.ts +70 -0
- package/server/setup-skills.ts +121 -0
- package/server/setup-tools.ts +162 -0
- package/server/shell.ts +68 -0
- package/server/stop-propagation.ts +365 -0
- package/server/tools-check.ts +118 -0
- package/server/trace-store.ts +1137 -0
- package/server/traces.ts +1356 -0
- package/server/worker-instructions.ts +9 -0
- package/server/workflow-steps.ts +422 -0
- package/shared/bead-ids.ts +25 -0
- package/shared/bm-fallback.ts +91 -0
- package/shared/bm-format.ts +424 -0
- package/shared/bm-questions.ts +213 -0
- package/shared/bm-report.ts +433 -0
- package/shared/contracts.ts +1371 -0
- package/shared/fallback-patterns.ts +201 -0
- package/shared/fallback.ts +46 -0
- package/shared/new-request.ts +20 -0
- package/shared/order.ts +22 -0
- package/shared/prices.ts +65 -0
- package/shared/settings.ts +57 -0
- package/shared/sole-worker.ts +20 -0
- package/shared/version.ts +6 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Switch to <candidate>" for a stopped Manager (delta 20260921 §4.5.2,
|
|
3
|
+
* REQ-066 c): the user is chatting with the Manager that stopped, so the
|
|
4
|
+
* plugin creates the replacement in the same workspace, and the user opens it
|
|
5
|
+
* from the usual entries (Beads Manager skips a replaced Manager).
|
|
6
|
+
*
|
|
7
|
+
* 1. The incident is still `pending` with a candidate (`fallback.act` runs one
|
|
8
|
+
* action at a time); the old Manager has no `bm.replacedBy`; the
|
|
9
|
+
* candidate's provider is available.
|
|
10
|
+
* 2. `BM-HANDOVER` role manager (`managerHandover`, 5 s budget).
|
|
11
|
+
* 3. `createManager` — the same path as `manager.ensure`, so the same
|
|
12
|
+
* instructions, Runtime facts and labels — on `bm-manager-fallback-<n>/<model>`,
|
|
13
|
+
* with the Manager rule's mode (`bm.modeSet` when one is chosen), the
|
|
14
|
+
* entry's thinking, `bm.replaces` = the old Manager, and the handover as
|
|
15
|
+
* its first message.
|
|
16
|
+
* 4. `bm.replacedBy` on the old Manager; a failure only logs.
|
|
17
|
+
* 5. Every live, non-replaced Worker of the workspace gets a `BM-SETTINGS`
|
|
18
|
+
* with the new Manager's id, through the notice queue.
|
|
19
|
+
* 6. `switched` with `replacementId`; the caller sends BM-FALLBACK into the
|
|
20
|
+
* old Manager's chat.
|
|
21
|
+
* 7. A creation that fails records `failed` and returns
|
|
22
|
+
* `E_FALLBACK_CREATE_FAILED`, never back to `pending`.
|
|
23
|
+
*/
|
|
24
|
+
import { peersOfWorkspace } from "./chat-peers";
|
|
25
|
+
import { createLocationResolver, resolveLocationFromPaseo } from "./collector";
|
|
26
|
+
import { bmAgentsOf, type DashboardPaseo } from "./dashboard-rpc";
|
|
27
|
+
import { REPLACED_BY_LABEL } from "./fallback-detect";
|
|
28
|
+
import { liveWorkersOf, managerHandover } from "./fallback-handover";
|
|
29
|
+
import { decidePending, type FallbackAction, type FallbackRpcDeps } from "./fallback-rpc";
|
|
30
|
+
import { readIncidents, replacementsOf } from "./fallback-state";
|
|
31
|
+
import { createManager, type ManagerPaseo } from "./manager";
|
|
32
|
+
import { enqueue as defaultEnqueue, type NoticeOutcome, type NoticePaseo } from "./notice-queue";
|
|
33
|
+
import { SETTINGS_NOTICE_MARKER } from "./notices";
|
|
34
|
+
import { setAgentLabels, type CliResult } from "./paseo-cli";
|
|
35
|
+
import { asRecord, availableProviders, nonEmpty, reasonOf } from "./role-choices";
|
|
36
|
+
import { currentInstructions } from "./role-extras";
|
|
37
|
+
import { capabilityOf, featuresFor, managerModeFor, modesFor, runPostureOf } from "./role-mode";
|
|
38
|
+
import { managerIdNotice } from "./settings-notices";
|
|
39
|
+
import type { TraceStoreLocation } from "./trace-store";
|
|
40
|
+
import { DashboardError, type FallbackIncident } from "../shared/contracts";
|
|
41
|
+
|
|
42
|
+
const REPLACES_LABEL = "bm.replaces";
|
|
43
|
+
const MODE_SET_LABEL = "bm.modeSet";
|
|
44
|
+
|
|
45
|
+
export interface ManagerSwitchDeps {
|
|
46
|
+
log?: (message: string) => void;
|
|
47
|
+
now?: () => Date;
|
|
48
|
+
/** `setAgentLabels` of `paseo-cli.ts` by default. */
|
|
49
|
+
setLabels?: (agentId: string, labels: Record<string, string>) => Promise<CliResult>;
|
|
50
|
+
enqueue?: (targetId: string, kind: string, text: string, paseo?: NoticePaseo) => Promise<NoticeOutcome>;
|
|
51
|
+
/** The Manager instructions; `currentInstructions("manager", paseo)` by default. */
|
|
52
|
+
readInstructions?: (paseo: unknown) => Promise<string>;
|
|
53
|
+
/** `managerHandover` by default. */
|
|
54
|
+
handover?: (incident: FallbackIncident, deps: { paseo: unknown; location: TraceStoreLocation | null; incidents: readonly FallbackIncident[] | null }) => Promise<string>;
|
|
55
|
+
/** The workspace trace store; the collector's resolver by default. */
|
|
56
|
+
location?: (paseo: unknown) => Promise<TraceStoreLocation | null>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The mode and features the replacement Manager starts with, by the candidate provider's capability. */
|
|
60
|
+
async function postureFor(
|
|
61
|
+
paseo: unknown,
|
|
62
|
+
candidate: NonNullable<FallbackIncident["candidate"]>,
|
|
63
|
+
log: (message: string) => void,
|
|
64
|
+
): Promise<{ modeId?: string; featureValues?: Record<string, unknown> }> {
|
|
65
|
+
const modes = await modesFor(paseo, candidate.alias, log);
|
|
66
|
+
const capability = capabilityOf(modes);
|
|
67
|
+
if (capability === "none") return {};
|
|
68
|
+
if (capability === "tiered") {
|
|
69
|
+
const modeId = managerModeFor(modes!, candidate.modeId);
|
|
70
|
+
return modeId === undefined ? {} : { modeId };
|
|
71
|
+
}
|
|
72
|
+
if (capability === "untiered") {
|
|
73
|
+
const features = await featuresFor(paseo, `${candidate.alias}/${candidate.model}`, undefined, log);
|
|
74
|
+
const posture = runPostureOf("manager", capability, modes!, features, candidate.modeId);
|
|
75
|
+
return {
|
|
76
|
+
...(typeof posture?.modeId === "string" ? { modeId: posture.modeId } : {}),
|
|
77
|
+
...(posture?.featureValues ? { featureValues: posture.featureValues } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return candidate.modeId === null ? {} : { modeId: candidate.modeId };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The `switch` action of `fallback.act` for a Manager. */
|
|
84
|
+
export function createManagerSwitch(deps: ManagerSwitchDeps = {}): FallbackAction {
|
|
85
|
+
const log = deps.log ?? ((message: string) => console.warn(message));
|
|
86
|
+
const setLabels = deps.setLabels ?? ((agentId: string, labels: Record<string, string>) => setAgentLabels(agentId, labels));
|
|
87
|
+
const enqueue = deps.enqueue ?? defaultEnqueue;
|
|
88
|
+
const readInstructions = deps.readInstructions ?? ((paseo: unknown) => currentInstructions("manager", paseo));
|
|
89
|
+
const handover = deps.handover ?? managerHandover;
|
|
90
|
+
const location = deps.location ?? createLocationResolver(resolveLocationFromPaseo);
|
|
91
|
+
|
|
92
|
+
return async (incident, paseo, rpcDeps: FallbackRpcDeps) => {
|
|
93
|
+
const now = rpcDeps.now ?? deps.now ?? (() => new Date());
|
|
94
|
+
const home = rpcDeps.home ?? null;
|
|
95
|
+
if (home === null) throw new DashboardError("E_FALLBACK_NOT_FOUND", "paseo-bm cannot find its install home");
|
|
96
|
+
if (incident.role !== "manager") throw new DashboardError("E_FALLBACK_NO_CANDIDATE", `not a Manager incident (${incident.role})`);
|
|
97
|
+
|
|
98
|
+
// 1. Still pending, with a candidate, and the old Manager not replaced yet.
|
|
99
|
+
const incidents = readIncidents(home, log).incidents;
|
|
100
|
+
const current = incidents.find((entry) => entry.id === incident.id);
|
|
101
|
+
if (current === undefined) throw new DashboardError("E_FALLBACK_NOT_FOUND", `no fallback incident ${incident.id}`);
|
|
102
|
+
if (current.status !== "pending") throw new DashboardError("E_FALLBACK_NOT_PENDING", `incident ${current.id} is ${current.status}, not pending`);
|
|
103
|
+
const candidate = current.candidate;
|
|
104
|
+
if (candidate === null) throw new DashboardError("E_FALLBACK_NO_CANDIDATE", "the fallback chain has no entry left for this Manager");
|
|
105
|
+
let labels: Record<string, unknown>;
|
|
106
|
+
try {
|
|
107
|
+
const ref = (paseo as { agents: { ref(id: string): { refresh(): Promise<{ agent?: unknown } | null> } } }).agents.ref(current.agentId);
|
|
108
|
+
labels = asRecord(asRecord((await ref.refresh())?.agent)?.["labels"]) ?? {};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// Nothing was created: the incident stays pending, so the user can try again.
|
|
111
|
+
throw new DashboardError("E_FALLBACK_CREATE_FAILED", `could not read Manager ${current.agentId}: ${reasonOf(error)}; try again`);
|
|
112
|
+
}
|
|
113
|
+
const replacedBy = nonEmpty(labels[REPLACED_BY_LABEL]);
|
|
114
|
+
if (replacedBy !== null) throw new DashboardError("E_FALLBACK_NOT_PENDING", `Manager ${current.agentId} was already replaced by ${replacedBy}`);
|
|
115
|
+
const available = await availableProviders(paseo);
|
|
116
|
+
if (available !== null && !available.has(candidate.baseProvider)) {
|
|
117
|
+
throw new DashboardError("E_FALLBACK_NO_CANDIDATE", `${candidate.baseProvider} is not available in Paseo right now`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. The handover, within its own budget.
|
|
121
|
+
const prompt = await handover(current, { paseo, location: await location(paseo).catch(() => null), incidents });
|
|
122
|
+
|
|
123
|
+
// 3. The replacement Manager, through the same path as manager.ensure.
|
|
124
|
+
const posture = await postureFor(paseo, candidate, log);
|
|
125
|
+
let created: { agentId: string };
|
|
126
|
+
try {
|
|
127
|
+
created = await createManager(paseo as ManagerPaseo, current.workspaceId, {
|
|
128
|
+
providerSelection: `${candidate.alias}/${candidate.model}`,
|
|
129
|
+
...(posture.modeId !== undefined ? { modeId: posture.modeId } : {}),
|
|
130
|
+
...(candidate.thinkingOptionId !== null ? { thinkingOptionId: candidate.thinkingOptionId } : {}),
|
|
131
|
+
...(posture.featureValues !== undefined ? { featureValues: posture.featureValues } : {}),
|
|
132
|
+
labels: { [REPLACES_LABEL]: current.agentId, ...(posture.modeId !== undefined ? { [MODE_SET_LABEL]: posture.modeId } : {}) },
|
|
133
|
+
prompt,
|
|
134
|
+
readInstructions: () => readInstructions(paseo),
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const detail = `could not create the fallback Manager on ${candidate.alias}/${candidate.model}: ${reasonOf(error)}`;
|
|
138
|
+
await decidePending(home, current.id, (entry) => ({ ...entry, status: "failed", decidedAt: now().toISOString(), error: detail }), log);
|
|
139
|
+
throw new DashboardError("E_FALLBACK_CREATE_FAILED", detail);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 4. Mark the old Manager; Beads Manager also skips it by the incident.
|
|
143
|
+
try {
|
|
144
|
+
const labelled = await setLabels(current.agentId, { [REPLACED_BY_LABEL]: created.agentId });
|
|
145
|
+
if (!labelled.ok) log(`[paseo-bm] could not label Manager ${current.agentId} as replaced by ${created.agentId}: ${labelled.reason}`);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
log(`[paseo-bm] could not label Manager ${current.agentId} as replaced by ${created.agentId}: ${reasonOf(error)}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 5. Every live Worker of the workspace reports to the new Manager from now on.
|
|
151
|
+
try {
|
|
152
|
+
const all = await bmAgentsOf(paseo as DashboardPaseo);
|
|
153
|
+
const peers = await peersOfWorkspace(all, current.workspaceId, async () => [], replacementsOf(incidents));
|
|
154
|
+
for (const worker of liveWorkersOf(peers)) {
|
|
155
|
+
const outcome = await enqueue(worker.id, SETTINGS_NOTICE_MARKER, managerIdNotice(created.agentId), paseo as NoticePaseo);
|
|
156
|
+
if (outcome === "dropped") log(`[paseo-bm] Worker ${worker.id} could not be told the new Manager ${created.agentId}.`);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
log(`[paseo-bm] telling the Workers of ${current.workspaceId} about the new Manager failed: ${reasonOf(error)}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 6. Decided.
|
|
163
|
+
return decidePending(
|
|
164
|
+
home,
|
|
165
|
+
current.id,
|
|
166
|
+
(entry) => ({ ...entry, status: "switched", decidedAt: now().toISOString(), replacementId: created.agentId }),
|
|
167
|
+
log,
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback for a stopped Reviewer (delta 20260921 §4.5.1, REQ-066 b).
|
|
3
|
+
*
|
|
4
|
+
* A Reviewer is created by its Worker, and Paseo wakes the Worker when the
|
|
5
|
+
* Reviewer ends a turn. A Reviewer the plugin created itself would never be
|
|
6
|
+
* heard by the Worker, so on **Switch** the plugin creates NO agent: it marks
|
|
7
|
+
* the incident `switched` and sends the parent Worker (through the notice
|
|
8
|
+
* queue) a `BM-FALLBACK` whose closing lines are the exact `create_agent`
|
|
9
|
+
* call to make — alias, mode, thinking, labels with `bm.replaces` — and the
|
|
10
|
+
* instruction to send the new Reviewer, unchanged, the review message the old
|
|
11
|
+
* one got.
|
|
12
|
+
*
|
|
13
|
+
* When that Reviewer appears (`agent.created`, `bm.replaces` = the old
|
|
14
|
+
* Reviewer of a `switched` incident), the incident records its id and the old
|
|
15
|
+
* Reviewer gets `bm.replacedBy`. If the notice queue was lost (a plugin reload)
|
|
16
|
+
* before the Worker got the instructions, the card offers **Resend to Worker**,
|
|
17
|
+
* which sends the same message again.
|
|
18
|
+
*/
|
|
19
|
+
import { aliasBases, decidePending, fallbackNotice, type FallbackAction, type FallbackRpcDeps } from "./fallback-rpc";
|
|
20
|
+
import { readIncidents, updateIncidents } from "./fallback-state";
|
|
21
|
+
import { FALLBACK_NOTICE_MARKER } from "./notices";
|
|
22
|
+
import { enqueue as defaultEnqueue, type NoticeOutcome, type NoticePaseo } from "./notice-queue";
|
|
23
|
+
import { setAgentLabels, type CliResult } from "./paseo-cli";
|
|
24
|
+
import { roleOfProvider } from "./agent-role";
|
|
25
|
+
import { asRecord, availableProviders, nonEmpty, reasonOf } from "./role-choices";
|
|
26
|
+
import { REVIEWER_FALLBACK_MODE, REVIEWER_FALLBACK_PROVIDERS, installHomeOf } from "./role-extras";
|
|
27
|
+
import { TIMED_OUT, capabilityOf, chooseModeId, featuresFor, modesFor, runPostureOf, withTimeout } from "./role-mode";
|
|
28
|
+
import { DashboardError, type FallbackIncident } from "../shared/contracts";
|
|
29
|
+
|
|
30
|
+
/** Label a replacement agent carries: the id of the agent it replaces. */
|
|
31
|
+
const REPLACES_LABEL = "bm.replaces";
|
|
32
|
+
const PARENT_AGENT_LABEL = "paseo.parent-agent-id";
|
|
33
|
+
const REQUEST_ID_LABEL = "bm.requestId";
|
|
34
|
+
const REPLACED_BY_LABEL = "bm.replacedBy";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The instructions the Worker gets for a switched Reviewer (agent-facing, so
|
|
38
|
+
* English, word for word). `mode` null → the "do not pass" wording (a provider
|
|
39
|
+
* without modes); `thinking` null → the thinking clause is left out.
|
|
40
|
+
*/
|
|
41
|
+
export function reviewerInstructions(incident: FallbackIncident, mode: string | null, thinking: string | null): string {
|
|
42
|
+
const candidate = incident.candidate!;
|
|
43
|
+
const old = incident.agentId;
|
|
44
|
+
return [
|
|
45
|
+
`The user chose to replace Reviewer ${old}. Create the new Reviewer now with create_agent:`,
|
|
46
|
+
`provider \`${candidate.alias}/${candidate.model}\`, ${mode === null ? "do not pass settings.modeId" : `settings.modeId \`${mode}\``},`,
|
|
47
|
+
`${thinking === null ? "" : `settings.thinkingOptionId \`${thinking}\`, `}and the labels you give any Reviewer plus \`${REPLACES_LABEL}\` = \`${old}\`.`,
|
|
48
|
+
`Send it, unchanged, the message you sent ${old}. This is the same review call, not a new one.`,
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The mode the replacement Reviewer is created in, for the candidate's alias:
|
|
54
|
+
* the posture rule for an untiered provider (§4.2.2), the hook's Reviewer rule
|
|
55
|
+
* for a tiered one (never `dangerous` / `planning`), null for a provider
|
|
56
|
+
* without modes. Modes that cannot be read: the entry's own mode, else `auto`
|
|
57
|
+
* on Claude or Codex, else null (Paseo then answers with its own list).
|
|
58
|
+
*/
|
|
59
|
+
export async function reviewerModeFor(
|
|
60
|
+
paseo: unknown,
|
|
61
|
+
candidate: NonNullable<FallbackIncident["candidate"]>,
|
|
62
|
+
log: (message: string) => void,
|
|
63
|
+
): Promise<string | null> {
|
|
64
|
+
const modes = await modesFor(paseo, candidate.alias, log);
|
|
65
|
+
const capability = capabilityOf(modes);
|
|
66
|
+
if (capability === "none") return null;
|
|
67
|
+
if (capability === "tiered") return chooseModeId("reviewer", modes!, undefined, candidate.modeId) ?? null;
|
|
68
|
+
if (capability === "untiered") {
|
|
69
|
+
const features = await featuresFor(paseo, `${candidate.alias}/${candidate.model}`, undefined, log);
|
|
70
|
+
return runPostureOf("reviewer", capability, modes!, features, candidate.modeId)?.modeId ?? candidate.modeId;
|
|
71
|
+
}
|
|
72
|
+
return candidate.modeId ?? (REVIEWER_FALLBACK_PROVIDERS.includes(candidate.baseProvider) ? REVIEWER_FALLBACK_MODE : null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ReviewerFallbackDeps {
|
|
76
|
+
log?: (message: string) => void;
|
|
77
|
+
now?: () => Date;
|
|
78
|
+
enqueue?: (targetId: string, kind: string, text: string, paseo?: NoticePaseo) => Promise<NoticeOutcome>;
|
|
79
|
+
/** `setAgentLabels` of `paseo-cli.ts` by default. */
|
|
80
|
+
setLabels?: (agentId: string, labels: Record<string, string>) => Promise<CliResult>;
|
|
81
|
+
/** The install home (tests); looked up otherwise. */
|
|
82
|
+
home?: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Sends the Worker its BM-FALLBACK with the instructions; the outcome of the queue. */
|
|
86
|
+
async function tellWorker(incident: FallbackIncident, paseo: unknown, deps: ReviewerFallbackDeps): Promise<NoticeOutcome> {
|
|
87
|
+
const log = deps.log ?? ((message: string) => console.warn(message));
|
|
88
|
+
const candidate = incident.candidate!;
|
|
89
|
+
const mode = await reviewerModeFor(paseo, candidate, log);
|
|
90
|
+
const bases = await aliasBases(paseo);
|
|
91
|
+
const text = fallbackNotice(incident, (alias) => bases[alias] ?? null, reviewerInstructions(incident, mode, candidate.thinkingOptionId));
|
|
92
|
+
const outcome = await (deps.enqueue ?? defaultEnqueue)(incident.parentId!, FALLBACK_NOTICE_MARKER, text, paseo as NoticePaseo);
|
|
93
|
+
if (outcome === "dropped") log(`[paseo-bm] the Worker ${incident.parentId} could not be told to replace Reviewer ${incident.agentId}; the card offers Resend to Worker.`);
|
|
94
|
+
return outcome;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The `switch` action for a Reviewer (§4.5.1): no agent is created. The
|
|
99
|
+
* incident must have a candidate whose provider is still available and a
|
|
100
|
+
* parent Worker; it becomes `switched` (no replacement yet) and the Worker is
|
|
101
|
+
* sent its instructions.
|
|
102
|
+
*/
|
|
103
|
+
export function createReviewerSwitch(deps: ReviewerFallbackDeps = {}): FallbackAction {
|
|
104
|
+
const log = deps.log ?? ((message: string) => console.warn(message));
|
|
105
|
+
return async (incident, paseo, rpcDeps: FallbackRpcDeps) => {
|
|
106
|
+
const now = rpcDeps.now ?? deps.now ?? (() => new Date());
|
|
107
|
+
const home = rpcDeps.home ?? null;
|
|
108
|
+
if (home === null) throw new DashboardError("E_FALLBACK_NOT_FOUND", "paseo-bm cannot find its install home");
|
|
109
|
+
if (incident.role !== "reviewer") throw new DashboardError("E_FALLBACK_NO_CANDIDATE", `not a Reviewer incident (${incident.role})`);
|
|
110
|
+
const candidate = incident.candidate;
|
|
111
|
+
if (candidate === null) throw new DashboardError("E_FALLBACK_NO_CANDIDATE", "the fallback chain has no entry left for this Reviewer");
|
|
112
|
+
if (incident.parentId === null) throw new DashboardError("E_FALLBACK_CREATE_FAILED", `the Worker of Reviewer ${incident.agentId} is not known`);
|
|
113
|
+
const available = await availableProviders(paseo);
|
|
114
|
+
if (available !== null && !available.has(candidate.baseProvider)) {
|
|
115
|
+
throw new DashboardError("E_FALLBACK_NO_CANDIDATE", `${candidate.baseProvider} is not available in Paseo right now`);
|
|
116
|
+
}
|
|
117
|
+
const switched = await decidePending(home, incident.id, (entry) => ({ ...entry, status: "switched", decidedAt: now().toISOString() }), log);
|
|
118
|
+
await tellWorker(switched, paseo, deps);
|
|
119
|
+
return switched;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The `resend` action (§7): the same instructions to the Worker again; the incident does not change. */
|
|
124
|
+
export function createReviewerResend(deps: ReviewerFallbackDeps = {}): FallbackAction {
|
|
125
|
+
return async (incident, paseo) => {
|
|
126
|
+
if (incident.candidate === null || incident.parentId === null) {
|
|
127
|
+
throw new DashboardError("E_FALLBACK_CREATE_FAILED", `nothing to resend for incident ${incident.id}`);
|
|
128
|
+
}
|
|
129
|
+
const outcome = await tellWorker(incident, paseo, deps);
|
|
130
|
+
if (outcome === "dropped") throw new DashboardError("E_FALLBACK_CREATE_FAILED", `the Worker ${incident.parentId} could not be reached`);
|
|
131
|
+
return incident;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* On `agent.created`: when the new agent is a Reviewer carrying `bm.replaces`
|
|
137
|
+
* = the old Reviewer of a `switched` incident without a replacement, record
|
|
138
|
+
* its id and label the old Reviewer `bm.replacedBy`. The label alone is not
|
|
139
|
+
* trusted (review b7): the new Reviewer must be in the incident's workspace,
|
|
140
|
+
* a child of the incident's Worker, and — when both carry one — of the same
|
|
141
|
+
* request; otherwise another workspace's or request's Reviewer could claim
|
|
142
|
+
* the incident and change its review count. Returns the incident it
|
|
143
|
+
* completed, or null. Never throws.
|
|
144
|
+
*/
|
|
145
|
+
export async function linkReplacementReviewer(
|
|
146
|
+
agentId: string,
|
|
147
|
+
provider: unknown,
|
|
148
|
+
paseo: unknown,
|
|
149
|
+
deps: ReviewerFallbackDeps = {},
|
|
150
|
+
): Promise<FallbackIncident | null> {
|
|
151
|
+
const log = deps.log ?? ((message: string) => console.warn(message));
|
|
152
|
+
try {
|
|
153
|
+
if (roleOfProvider(provider) !== "reviewer") return null;
|
|
154
|
+
const ref = (paseo as { agents: { ref(id: string): { refresh(): Promise<{ agent?: unknown } | null> } } }).agents.ref(agentId);
|
|
155
|
+
const snapshot = asRecord((await ref.refresh())?.agent);
|
|
156
|
+
const labels = asRecord(snapshot?.["labels"]) ?? {};
|
|
157
|
+
const replaces = nonEmpty(labels[REPLACES_LABEL]);
|
|
158
|
+
if (replaces === null) return null;
|
|
159
|
+
const workspaceId = nonEmpty(snapshot?.["workspaceId"]);
|
|
160
|
+
const parentId = nonEmpty(labels[PARENT_AGENT_LABEL]) ?? nonEmpty(snapshot?.["parentAgentId"]);
|
|
161
|
+
const requestId = nonEmpty(labels[REQUEST_ID_LABEL]);
|
|
162
|
+
let home = deps.home;
|
|
163
|
+
if (home === undefined) {
|
|
164
|
+
const found = await withTimeout(installHomeOf(paseo));
|
|
165
|
+
home = found === TIMED_OUT ? null : found;
|
|
166
|
+
}
|
|
167
|
+
if (home === null) return null;
|
|
168
|
+
const match = (incident: FallbackIncident) =>
|
|
169
|
+
incident.role === "reviewer" &&
|
|
170
|
+
incident.status === "switched" &&
|
|
171
|
+
incident.agentId === replaces &&
|
|
172
|
+
incident.replacementId === null &&
|
|
173
|
+
// Same workspace, same Worker, and the same request when both name one.
|
|
174
|
+
workspaceId === incident.workspaceId &&
|
|
175
|
+
parentId !== null &&
|
|
176
|
+
parentId === incident.parentId &&
|
|
177
|
+
(requestId === null || incident.requestId === null || requestId === incident.requestId);
|
|
178
|
+
if (!readIncidents(home, log).incidents.some(match)) return null;
|
|
179
|
+
let linked: FallbackIncident | null = null;
|
|
180
|
+
await updateIncidents(
|
|
181
|
+
home,
|
|
182
|
+
(incidents) => {
|
|
183
|
+
const index = incidents.findIndex(match);
|
|
184
|
+
if (index === -1) return null;
|
|
185
|
+
linked = { ...incidents[index]!, replacementId: agentId };
|
|
186
|
+
return incidents.map((incident, at) => (at === index ? linked! : incident));
|
|
187
|
+
},
|
|
188
|
+
log,
|
|
189
|
+
);
|
|
190
|
+
if (linked === null) return null;
|
|
191
|
+
const labelled = await (deps.setLabels ?? ((id: string, labels: Record<string, string>) => setAgentLabels(id, labels)))(replaces, { [REPLACED_BY_LABEL]: agentId });
|
|
192
|
+
if (!labelled.ok) log(`[paseo-bm] could not label Reviewer ${replaces} as replaced by ${agentId}: ${labelled.reason}`);
|
|
193
|
+
return linked;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
log(`[paseo-bm] linking the replacement Reviewer ${agentId} failed: ${reasonOf(error)}`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|