@coreplane/switchboard 1.248.0 → 1.249.1
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/dist/assets/config/config.example.yaml +44 -18
- package/dist/assets/deploy/cloudflare/preflight.mjs +21 -19
- package/dist/assets/deploy/cloudflare-memory/worker.ts +31 -0
- package/dist/assets/deploy/cloudflare-resident/drain.ts +109 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +5 -3
- package/dist/assets/deploy/cloudflare-resident/threadErr.ts +32 -3
- package/dist/assets/deploy/cloudflare-resident/worker.ts +235 -13
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +56 -9
- package/dist/assets/deploy/secrets.manifest.json +6 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +4 -4
- package/dist/assets/src/core/budgets.ts +24 -0
- package/dist/assets/src/core/coordinator/contract.ts +4 -3
- package/dist/assets/src/core/coordinator/driver.ts +40 -7
- package/dist/assets/src/core/modelCard.ts +348 -0
- package/dist/assets/src/core/modelPricing.ts +14 -5
- package/dist/assets/src/core/modelRegistry.ts +51 -0
- package/dist/assets/src/core/provider.ts +103 -0
- package/dist/assets/src/core/refusal.ts +181 -0
- package/dist/assets/src/core/runEvents.ts +43 -0
- package/dist/assets/src/core/ship/coordinator.ts +80 -12
- package/dist/assets/src/core/ship/handoff.ts +54 -19
- package/dist/assets/src/core/trace/workerTrace.ts +9 -3
- package/dist/assets/src/core/types.ts +327 -0
- package/dist/assets/src/deploy/liveGate.ts +35 -0
- package/dist/assets/src/deploy/restart.ts +12 -11
- package/dist/assets/src/execution/residentRefresh.ts +26 -1
- package/dist/assets/src/execution/sandboxErrors.ts +122 -4
- package/dist/assets/web/dist/.vite/manifest.json +30 -30
- package/dist/assets/web/dist/assets/DeliveryPage-DF4aQypG.js +1 -0
- package/dist/assets/web/dist/assets/{HomePage-DYxC0izY.js → HomePage-BpQRky8B.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-DLIpWYOc.js → ResidentDetailPage-BIUXyz6K.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-6LipuDjR.js → ResidentsIndexPage-BZymgSAb.js} +1 -1
- package/dist/assets/web/dist/assets/{RunFoldRow-V-iSy64e.js → RunFoldRow-3m4CPRI4.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-DUalB1u2.js → RunRoutePage-bgkjkA0p.js} +3 -3
- package/dist/assets/web/dist/assets/{RunsIndexPage-B9Ba1KdD.js → RunsIndexPage-8S944AzB.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-KdjLtD_7.js → ScheduledPage-8bBtG9y3.js} +1 -1
- package/dist/assets/web/dist/assets/{SettingsPage-IT5l_NaL.js → SettingsPage-DQeNvfaV.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-DBHAl4Il.js → StatusDot-BPE5syBa.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-_LEjptLV.js → Tooltip-DkoeZfTs.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-BUzw--Ii.js +1 -0
- package/dist/assets/web/dist/assets/{dist-BcYPGOBL.js → dist-D11y9ZJ4.js} +1 -1
- package/dist/assets/web/dist/assets/{main-DUfSE0dj.js → main-B6LcgNM6.js} +2 -2
- package/dist/cli.js +2475 -1021
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-NP4g6bQd.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-DPBsvGPR.js +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The pi registry as the card's catalog (record 0052): the JSON files the
|
|
2
|
+
// installed `@earendil-works/pi-ai` ships under `providers/data/`, keyed
|
|
3
|
+
// catalog file → wire → model id. A block's `catalog` names the file consulted
|
|
4
|
+
// for its cards (default, the block's own name when such a file exists; `none`
|
|
5
|
+
// for no catalog); a card is looked up under the block's wire first (our
|
|
6
|
+
// `openai-chat` is pi's `openai-completions`), then under any wire, since a
|
|
7
|
+
// card of another wire still carries levels, window and price. Nothing here
|
|
8
|
+
// knows a vendor or a model by name — and nothing here touches Node: this
|
|
9
|
+
// module is the seam's types alone, reachable from the Workers' typechecks
|
|
10
|
+
// through the run events' card type. The file-reading implementation is the
|
|
11
|
+
// bot's ./installedModelRegistry.ts; tests hand an in-memory table.
|
|
12
|
+
|
|
13
|
+
import type { Wire } from "./provider.js";
|
|
14
|
+
|
|
15
|
+
/** One card as pi's registry files carry it (only the fields the bot reads). */
|
|
16
|
+
export interface RegistryCard {
|
|
17
|
+
id?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
/** pi's own wire word (`anthropic-messages`, `openai-completions`, `openai-responses`). */
|
|
20
|
+
api?: string;
|
|
21
|
+
reasoning?: boolean;
|
|
22
|
+
/** Our tiers as pi spells them: `null` refuses a level, an absent tier is
|
|
23
|
+
* not named (the card's reader applies pi's rule). */
|
|
24
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
25
|
+
input?: string[];
|
|
26
|
+
cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
|
|
27
|
+
contextWindow?: number;
|
|
28
|
+
maxTokens?: number;
|
|
29
|
+
compat?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One registry file: pi's wire word → model id → card. */
|
|
33
|
+
export type RegistryFile = Record<string, Record<string, RegistryCard>>;
|
|
34
|
+
|
|
35
|
+
/** The catalog seam the card resolver reads (record 0052): a card by catalog
|
|
36
|
+
* name and wire, or by any wire; a catalog that does not exist reads as no
|
|
37
|
+
* card. The production implementation reads pi's installed files; tests hand
|
|
38
|
+
* a table. */
|
|
39
|
+
export interface ModelRegistry {
|
|
40
|
+
/** The card for `model` in `catalog`, under `wire` first then any wire. */
|
|
41
|
+
card(catalog: string | undefined, wire: Wire, model: string): RegistryCard | undefined;
|
|
42
|
+
/** The file named, or undefined when the catalog does not exist. */
|
|
43
|
+
file(catalog: string): RegistryFile | undefined;
|
|
44
|
+
/** Every catalog file name the library ships. */
|
|
45
|
+
names(): string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Our wire word as pi's registry spells it. */
|
|
49
|
+
export function piWireOf(wire: Wire): string {
|
|
50
|
+
return wire === "openai-chat" ? "openai-completions" : wire;
|
|
51
|
+
}
|
|
@@ -93,14 +93,117 @@ export interface Provider {
|
|
|
93
93
|
complete(req: CompletionRequest): Promise<CompletionResult>;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** The three wire shapes a provider block may declare (record 0052):
|
|
97
|
+
* Anthropic's Messages API, OpenAI's Chat Completions and OpenAI's Responses
|
|
98
|
+
* API. `openai-responses` is a declaration only in this slice — the proxy
|
|
99
|
+
* still serves two routes (a later slice adds the third) — and a block that names it
|
|
100
|
+
* keeps routing through the compatible shape until then. */
|
|
101
|
+
export const WIRES = ["anthropic-messages", "openai-chat", "openai-responses"] as const;
|
|
102
|
+
export type Wire = (typeof WIRES)[number];
|
|
103
|
+
|
|
104
|
+
/** The legacy `type` words, as the wires they load as for one release. */
|
|
105
|
+
export const WIRE_ALIASES: Readonly<Record<string, Wire>> = {
|
|
106
|
+
anthropic: "anthropic-messages",
|
|
107
|
+
"openai-compatible": "openai-chat",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** One model's operator override under a block's `models.<id>` (record 0052,
|
|
111
|
+
* the operator layer of the card). Every field is optional and wins over the
|
|
112
|
+
* registry card and the wire defaults where it is set. */
|
|
113
|
+
export interface ProviderModelOverride {
|
|
114
|
+
/** Our effort tiers → the wire's word, or null to refuse the tier. */
|
|
115
|
+
levels?: Record<string, string | null>;
|
|
116
|
+
/** The body field the output cap is spelled with on this model. */
|
|
117
|
+
capField?: string;
|
|
118
|
+
/** The model's context window in tokens. */
|
|
119
|
+
window?: number;
|
|
120
|
+
/** Which input kinds the model takes. */
|
|
121
|
+
inputs?: { image?: boolean; document?: boolean };
|
|
122
|
+
/** The model's cache rule. */
|
|
123
|
+
cache?: "automatic" | "markers" | "none" | "unknown";
|
|
124
|
+
/** USD per million tokens, by kind. */
|
|
125
|
+
price?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
export interface ProviderConfig {
|
|
129
|
+
/** The legacy wire word: `anthropic` or `openai-compatible` (record
|
|
130
|
+
* 0052). `wire` is the new spelling; `type` keeps loading as its alias for
|
|
131
|
+
* one release. A block may declare either; validation derives the one it
|
|
132
|
+
* did not. */
|
|
97
133
|
type: "anthropic" | "openai-compatible";
|
|
134
|
+
/** The wire the block speaks (`anthropic-messages`, `openai-chat`,
|
|
135
|
+
* `openai-responses`). Absent → derived from `type`. */
|
|
136
|
+
wire?: Wire;
|
|
137
|
+
/** The vendor whose models this block serves: a name (default, the block's
|
|
138
|
+
* own) or `model`, which makes the block an aggregator whose vendor is the
|
|
139
|
+
* model id's first segment. */
|
|
140
|
+
vendor?: string;
|
|
141
|
+
/** The pi registry file consulted for this block's cards (default, the
|
|
142
|
+
* block's own name when such a file exists; `none` otherwise). */
|
|
143
|
+
catalog?: string;
|
|
144
|
+
/** Per-model operator overrides, keyed by the model id as the ref spells it. */
|
|
145
|
+
models?: Record<string, ProviderModelOverride>;
|
|
146
|
+
/** Extra body fields merged into every request on the wires whose adapter
|
|
147
|
+
* takes one. Never a control: not decided, not noted, not in the matrix. */
|
|
148
|
+
passthrough?: Record<string, unknown>;
|
|
98
149
|
/** Env var holding the API key (never put keys in config files). */
|
|
99
150
|
apiKeyEnv?: string;
|
|
100
151
|
/** Base URL for openai-compatible providers (e.g. http://localhost:11434/v1). */
|
|
101
152
|
baseUrl?: string;
|
|
102
153
|
}
|
|
103
154
|
|
|
155
|
+
/** The wire a block speaks: its `wire` when declared, else its legacy `type`. */
|
|
156
|
+
export function wireOf(block: Pick<ProviderConfig, "type" | "wire">): Wire {
|
|
157
|
+
return block.wire ?? WIRE_ALIASES[block.type] ?? "openai-chat";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One `<block>/<model>` ref read for the vendor it serves (record 0052):
|
|
161
|
+
* the block, the model id as the ref spells it, the vendor, the vendor's own
|
|
162
|
+
* id (the model id less a vendor prefix) and which layer named the vendor. */
|
|
163
|
+
export interface VendorRef {
|
|
164
|
+
block: string;
|
|
165
|
+
model: string;
|
|
166
|
+
vendor: string;
|
|
167
|
+
vendorId: string;
|
|
168
|
+
vendorSource: "declared" | "model" | "block";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The one vendor parse (record 0052): `parseModelRef` is the only other parser of a
|
|
172
|
+
* ref. A block that declares a vendor name uses it; a block that declares
|
|
173
|
+
* `vendor: model`, or a model id that carries its own vendor prefix
|
|
174
|
+
* (`openrouter/anthropic/claude-sonnet-5`), reads the vendor off the id's
|
|
175
|
+
* first segment; otherwise the block's own name is the vendor. `catalog`
|
|
176
|
+
* never enters: a block named unlike its catalog still serves the vendor the
|
|
177
|
+
* declaration or the id names. */
|
|
178
|
+
export function vendorOf(
|
|
179
|
+
ref: string,
|
|
180
|
+
blocks: Readonly<Record<string, Pick<ProviderConfig, "vendor">>> = {},
|
|
181
|
+
): VendorRef {
|
|
182
|
+
const { provider: block, model } = parseModelRef(ref);
|
|
183
|
+
const declared = blocks[block]?.vendor;
|
|
184
|
+
const slash = model.indexOf("/");
|
|
185
|
+
if (declared !== undefined && declared !== "model") {
|
|
186
|
+
const prefix = `${declared}/`;
|
|
187
|
+
return {
|
|
188
|
+
block,
|
|
189
|
+
model,
|
|
190
|
+
vendor: declared,
|
|
191
|
+
vendorId: model.startsWith(prefix) ? model.slice(prefix.length) : model,
|
|
192
|
+
vendorSource: "declared",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (slash > 0 && (declared === "model" || declared === undefined)) {
|
|
196
|
+
return {
|
|
197
|
+
block,
|
|
198
|
+
model,
|
|
199
|
+
vendor: model.slice(0, slash),
|
|
200
|
+
vendorId: model.slice(slash + 1),
|
|
201
|
+
vendorSource: "model",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { block, model, vendor: block, vendorId: model, vendorSource: "block" };
|
|
205
|
+
}
|
|
206
|
+
|
|
104
207
|
/** The env var Anthropic's own SDK reads when an `anthropic` provider block names none. */
|
|
105
208
|
export const ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
|
|
106
209
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The refusal seam (docs/decisions/0054-a-refusal-the-person-caused-is-one-question-with-a-best-guess.md): every refusal the
|
|
2
|
+
// bot makes is one `Refusal` value with a cause, produced by the stage that
|
|
3
|
+
// cannot proceed and rendered in one place (`renderRefusal` in
|
|
4
|
+
// dispatch/reply.ts). A code has exactly one cause, in one table — a code the
|
|
5
|
+
// table does not know is a type error — so the trace, the run record and the
|
|
6
|
+
// door report count refusals by the same names.
|
|
7
|
+
import type { IncomingMessage } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** Why the bot refused, in the record's three classes: a different sentence
|
|
10
|
+
* from the person would work (`request`), the person may not (`policy`), or
|
|
11
|
+
* the bot or a dependency cannot act now (`system`). */
|
|
12
|
+
export type RefusalCause = "request" | "policy" | "system";
|
|
13
|
+
|
|
14
|
+
/** The bot's best guess at what the person meant: the person's message with
|
|
15
|
+
* the fix applied, the line as the person would type it, and the evidence
|
|
16
|
+
* that names the match. Produced by a later unit of record 0054's plan;
|
|
17
|
+
* carried here so the seam's
|
|
18
|
+
* shape is complete from the first unit. */
|
|
19
|
+
export interface Guess {
|
|
20
|
+
proposal: IncomingMessage;
|
|
21
|
+
line: string;
|
|
22
|
+
evidence: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One cause per code — the closed table `causeOf` reads. The codes are the
|
|
26
|
+
* inventory's: the dispatch gates' `refuse(code)` outcomes, the click's four
|
|
27
|
+
* `confirmation_*` codes, the two `elsewhere_*` reasons that never reached a
|
|
28
|
+
* span before, the references step's eight reasons (request 2, policy 3,
|
|
29
|
+
* system 3 — the appendix's split), and `uncaught` for the catch-all. */
|
|
30
|
+
const CAUSE_OF = {
|
|
31
|
+
// the dispatch gates (dispatcher.ts `refuse(code)`)
|
|
32
|
+
agent_allowlist: "policy",
|
|
33
|
+
profile_bounded: "policy",
|
|
34
|
+
repo_not_visible: "policy",
|
|
35
|
+
repo_unverified: "system",
|
|
36
|
+
repo_not_onboarded: "request",
|
|
37
|
+
repo_access: "policy",
|
|
38
|
+
pr_head_unknown: "system",
|
|
39
|
+
branch_moved: "system",
|
|
40
|
+
coordinator_thread_live: "system",
|
|
41
|
+
live_agent_allowlist: "policy",
|
|
42
|
+
follow_up_refused: "request",
|
|
43
|
+
elsewhere_agent_allowlist: "policy",
|
|
44
|
+
elsewhere_follow_up_refused: "request",
|
|
45
|
+
which_branch: "request",
|
|
46
|
+
workspace_lost: "system",
|
|
47
|
+
ship_budget: "request",
|
|
48
|
+
setup_failed: "system",
|
|
49
|
+
// the click on a confirmation (confirm.ts)
|
|
50
|
+
confirmation_used: "request",
|
|
51
|
+
confirmation_expired: "request",
|
|
52
|
+
confirmation_foreign: "policy",
|
|
53
|
+
confirmation_unreadable: "system",
|
|
54
|
+
// the references step's eight reasons (record 0037: one sentence, eight codes)
|
|
55
|
+
reference_over_cap: "request",
|
|
56
|
+
reference_rate_limited: "request",
|
|
57
|
+
reference_guest: "policy",
|
|
58
|
+
reference_not_a_member: "policy",
|
|
59
|
+
reference_denied: "policy",
|
|
60
|
+
reference_timed_out: "system",
|
|
61
|
+
reference_never: "system",
|
|
62
|
+
reference_fetch_failed: "system",
|
|
63
|
+
// the catch-all: an uncaught throw in dispatch()
|
|
64
|
+
uncaught: "system",
|
|
65
|
+
// the ship preflight's nine results (record 0054's plan): the old
|
|
66
|
+
// `ship_preflight` code is kept as these codes' prefix, so a query on the
|
|
67
|
+
// old code still finds them; the sentence did not split with the code.
|
|
68
|
+
ship_preflight_channel: "system",
|
|
69
|
+
ship_preflight_permission: "policy",
|
|
70
|
+
ship_preflight_no_repo: "request",
|
|
71
|
+
ship_preflight_pr_unreachable: "system",
|
|
72
|
+
ship_preflight_pr_facts: "system",
|
|
73
|
+
ship_preflight_fork_head: "request",
|
|
74
|
+
ship_preflight_head_unknown: "system",
|
|
75
|
+
ship_preflight_closed_resume: "request",
|
|
76
|
+
ship_preflight_no_task: "request",
|
|
77
|
+
// the plan hand-off's fifteen sentences (two share `plan_history_unavailable`)
|
|
78
|
+
plan_base_unknown: "request",
|
|
79
|
+
plan_routed_seed: "request",
|
|
80
|
+
plan_id_invalid: "request",
|
|
81
|
+
plan_unreadable: "request",
|
|
82
|
+
plan_no_units: "request",
|
|
83
|
+
plan_units_unknown: "request",
|
|
84
|
+
plan_runner_state_unknown: "system",
|
|
85
|
+
plan_runner_live: "system",
|
|
86
|
+
plan_runner_state_unread: "system",
|
|
87
|
+
plan_units_merged: "request",
|
|
88
|
+
plan_history_unavailable: "system",
|
|
89
|
+
plan_runner_conflict: "system",
|
|
90
|
+
plan_instance_orphaned: "system",
|
|
91
|
+
plan_start_failed: "system",
|
|
92
|
+
// the directive and resolve parsers' thrown errors (A6 of the inventory)
|
|
93
|
+
directive_agent: "request",
|
|
94
|
+
directive_effort: "request",
|
|
95
|
+
directive_budget: "request",
|
|
96
|
+
directive_severity: "request",
|
|
97
|
+
directive_renewals: "request",
|
|
98
|
+
provider_unknown: "request",
|
|
99
|
+
// the model card's refusal (record 0052, model-proxy item 11): a control the
|
|
100
|
+
// resolved card does not take, named before any card or model call
|
|
101
|
+
model_card_refused: "request",
|
|
102
|
+
// a follow-up dropped because the run it was folded into was stopped
|
|
103
|
+
follow_up_dropped: "system",
|
|
104
|
+
// the typed-command codes (`InvokeErrorCode`): `chatErrorLine` renders these
|
|
105
|
+
// through the renderer's one line shape; the cause rides `CommandError`.
|
|
106
|
+
command_unauthorized: "policy",
|
|
107
|
+
command_invalid_input: "request",
|
|
108
|
+
command_not_found: "request",
|
|
109
|
+
command_conflict: "system",
|
|
110
|
+
command_unavailable: "system",
|
|
111
|
+
command_busy: "system",
|
|
112
|
+
command_internal: "system",
|
|
113
|
+
// the resident Worker's JSON errors, at the moment the bot receives them
|
|
114
|
+
// (record 0054): the cause is read from the Worker's `error` prefix.
|
|
115
|
+
resident_attach_rejected: "request",
|
|
116
|
+
resident_attach_failed: "system",
|
|
117
|
+
} as const satisfies Record<string, RefusalCause>;
|
|
118
|
+
|
|
119
|
+
/** Every code a refusal may carry. A new refusal site adds its code here with
|
|
120
|
+
* its one cause; an unknown code does not compile. */
|
|
121
|
+
export type RefusalCode = keyof typeof CAUSE_OF;
|
|
122
|
+
|
|
123
|
+
/** The one cause of a code — exhaustive over the union above. */
|
|
124
|
+
export function causeOf(code: RefusalCode): RefusalCause {
|
|
125
|
+
return CAUSE_OF[code];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Every code the table knows, for tests and reports. */
|
|
129
|
+
export const REFUSAL_CODES: readonly RefusalCode[] = Object.keys(CAUSE_OF) as RefusalCode[];
|
|
130
|
+
|
|
131
|
+
/** One refusal: the cause, the code, the sentence the person reads (built by
|
|
132
|
+
* the producing site, byte-identical to what it said before the seam), the
|
|
133
|
+
* best guess when the site holds one, and the way forward when the cause is
|
|
134
|
+
* `policy` and the text does not already carry it. */
|
|
135
|
+
export interface Refusal {
|
|
136
|
+
cause: RefusalCause;
|
|
137
|
+
code: RefusalCode;
|
|
138
|
+
text: string;
|
|
139
|
+
guess?: Guess;
|
|
140
|
+
wayForward?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Build a `Refusal` for a code: the cause comes from the one table. */
|
|
144
|
+
export function refusalOf(code: RefusalCode, text: string, extra?: Pick<Refusal, "guess" | "wayForward">): Refusal {
|
|
145
|
+
return { cause: causeOf(code), code, text, ...(extra ?? {}) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The one line a `Refusal` reads as, shared by the async renderer and the
|
|
149
|
+
* string-shaped surfaces (`chatErrorLine`): the producer's own text, plus the
|
|
150
|
+
* way forward when the cause is `policy` and one was set apart. */
|
|
151
|
+
export function refusalLine(refusal: Refusal): string {
|
|
152
|
+
if (refusal.cause === "policy" && refusal.wayForward) return `${refusal.text} ${refusal.wayForward}`;
|
|
153
|
+
return refusal.text;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A typed command's error code as a refusal code with its one cause. */
|
|
157
|
+
export function commandRefusalCode(
|
|
158
|
+
code: "unauthorized" | "invalid_input" | "not_found" | "conflict" | "unavailable" | "busy" | "internal",
|
|
159
|
+
): RefusalCode {
|
|
160
|
+
return `command_${code}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The cause the Worker's `error` prefix names (record 0054): a wrong or missing ref is
|
|
164
|
+
* the person's to fix (`request`); everything else is the machinery's —
|
|
165
|
+
* `op-unavailable` included: the Worker says it when its command table has no
|
|
166
|
+
* entry for the op the bot sent, a version skew between bot and Worker, not a
|
|
167
|
+
* sentence the person could reword. */
|
|
168
|
+
export function residentErrorCause(error: string): RefusalCause {
|
|
169
|
+
const prefix = error.split(":", 1)[0]?.trim();
|
|
170
|
+
return prefix === "needs-ref" || prefix === "unknown-ref" ? "request" : "system";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A refusal as a throwable, for producers whose call shape is a throw. */
|
|
174
|
+
export class RefusalError extends Error {
|
|
175
|
+
readonly refusal: Refusal;
|
|
176
|
+
constructor(refusal: Refusal) {
|
|
177
|
+
super(refusal.text);
|
|
178
|
+
this.name = "RefusalError";
|
|
179
|
+
this.refusal = refusal;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// tsconfigs — importing prDescription.ts would drag zod into those graphs.
|
|
4
4
|
import type { PrDescription, RenderedPointer } from "./prDescriptionTypes.js";
|
|
5
5
|
import type { HarnessScope } from "./harness/scope.js";
|
|
6
|
+
import type { ModelCard } from "./modelCard.js";
|
|
6
7
|
|
|
7
8
|
/** The `pr_description` review artifact minus the event envelope
|
|
8
9
|
* (docs/reference/specs/reading-diff.md item 7). */
|
|
@@ -122,12 +123,24 @@ export type RunNoteKind =
|
|
|
122
123
|
* (docs/reference/specs/run-history.md item 37); the summary says how many calls were
|
|
123
124
|
* in flight at the kill and how each was settled. Published by the runner. */
|
|
124
125
|
| "resumed"
|
|
126
|
+
/** A control was decided against the model card and is not native (record
|
|
127
|
+
* 0052): a fallback (`applied` differs from `asked`, `vouched` true) or
|
|
128
|
+
* an unvouched send (`vouched` false). One note per degraded control,
|
|
129
|
+
* published by the dispatcher before the first turn. `why` says what the
|
|
130
|
+
* card could not vouch for; `summary` is the human line. */
|
|
131
|
+
| "control_degraded"
|
|
125
132
|
/** What the run's session seed could not do (docs/reference/specs/session-log.md
|
|
126
133
|
* item 9): the log could not be read so the run seeds from the channel, the
|
|
127
134
|
* newest turn alone was over the seed budget, the previous run's end was
|
|
128
135
|
* unknown so no line since could be told apart. One note per reason,
|
|
129
136
|
* published by the dispatcher before the first turn. */
|
|
130
137
|
| "seed"
|
|
138
|
+
/** This run is a question's Yes (record 0054;
|
|
139
|
+
* docs/reference/specs/run-history.md item 2): the stored proposal went
|
|
140
|
+
* back through `dispatch()` as the requester, and the summary names the
|
|
141
|
+
* question's refusal code. Published by the dispatcher before the first
|
|
142
|
+
* turn. */
|
|
143
|
+
| "redispatch"
|
|
131
144
|
/** A coding run pushed onto a branch that already heads an open PR without
|
|
132
145
|
* resubmitting the PR description, and the same run is being given one
|
|
133
146
|
* bounded extra model turn to submit it (docs/reference/specs/pr-description.md
|
|
@@ -217,6 +230,18 @@ export type RunNoteKind =
|
|
|
217
230
|
* item 7): the summary names the tool and the rule; the model read the same
|
|
218
231
|
* reason as the tool's result. Published by the bot's authorize route. */
|
|
219
232
|
| "tool_refused"
|
|
233
|
+
/** OpenCode withdrew a pending ask before the gate's reply to it landed
|
|
234
|
+
* (harness.md item 2): the server answered the reply 404 and its pending
|
|
235
|
+
* asks no longer listed the ask — the gate refused a sibling call of the
|
|
236
|
+
* same step, and at a reject the binary declines every other pending ask
|
|
237
|
+
* (`packages/core/src/permission.ts:203-220` at the pinned v2.0.3) and
|
|
238
|
+
* ends their step (measured in `opencode/testing/realDriver.test.ts`).
|
|
239
|
+
* The summary names the call, the reply the gate had decided and the
|
|
240
|
+
* sibling's refusal when the step has one (or says no refusal is on the
|
|
241
|
+
* record). Information, not a failure: nothing ran that the gate did not
|
|
242
|
+
* decide, the loop is not stopped, and the step ends by the server's word.
|
|
243
|
+
* Published by the OpenCode bridge. */
|
|
244
|
+
| "ask_withdrawn"
|
|
220
245
|
/** An OpenCode tool settled under a step this loop never saw start and the
|
|
221
246
|
* settle was set aside (harness.md item 13): an earlier execution's late
|
|
222
247
|
* result — the pinned binary's ordinary shape after a hung call's interrupt
|
|
@@ -269,7 +294,9 @@ export const RUN_NOTE_KINDS = [
|
|
|
269
294
|
"mcp_unavailable",
|
|
270
295
|
"follow_up",
|
|
271
296
|
"resumed",
|
|
297
|
+
"control_degraded",
|
|
272
298
|
"seed",
|
|
299
|
+
"redispatch",
|
|
273
300
|
"description_turn",
|
|
274
301
|
"verdict_turn",
|
|
275
302
|
"cold_sandbox",
|
|
@@ -283,6 +310,7 @@ export const RUN_NOTE_KINDS = [
|
|
|
283
310
|
"harness_error",
|
|
284
311
|
"policy_refusal",
|
|
285
312
|
"tool_refused",
|
|
313
|
+
"ask_withdrawn",
|
|
286
314
|
"settle_set_aside",
|
|
287
315
|
"tool_unnamed",
|
|
288
316
|
"directory_reached",
|
|
@@ -389,6 +417,7 @@ export function isHeadMaterial(event: RunEvent): boolean {
|
|
|
389
417
|
event.kind === "mcp_unavailable" ||
|
|
390
418
|
event.kind === "spans_dropped" ||
|
|
391
419
|
event.kind === "cold_sandbox" ||
|
|
420
|
+
event.kind === "control_degraded" ||
|
|
392
421
|
event.kind === "ledger_untracked" ||
|
|
393
422
|
event.kind === "rebind_refused"
|
|
394
423
|
);
|
|
@@ -525,6 +554,14 @@ export type RunEvent =
|
|
|
525
554
|
summary: string;
|
|
526
555
|
mode?: StopMode;
|
|
527
556
|
actor?: RunActor;
|
|
557
|
+
/** On a `control_degraded` note only (record 0052): which control,
|
|
558
|
+
* the word asked, the word applied, whether a layer vouched for the
|
|
559
|
+
* applied word, and what the card could not vouch for. */
|
|
560
|
+
control?: string;
|
|
561
|
+
asked?: string;
|
|
562
|
+
applied?: string;
|
|
563
|
+
vouched?: boolean;
|
|
564
|
+
why?: string;
|
|
528
565
|
/** On a `spans_dropped` note only (docs/reference/specs/tracing.md): the runner-clock
|
|
529
566
|
* interval the dropped setup records covered — a `not recorded` loss. */
|
|
530
567
|
from?: number;
|
|
@@ -624,6 +661,12 @@ export type RunEvent =
|
|
|
624
661
|
* written before the word was a scope setting. */
|
|
625
662
|
harnessScope?: HarnessScope;
|
|
626
663
|
effort?: string;
|
|
664
|
+
/** The model card this run resolved before its first call (record
|
|
665
|
+
* 0052): the wire, vendor, levels, cap field, window, inputs, cache rule
|
|
666
|
+
* and price source, each with the layer that named it. Absent on a
|
|
667
|
+
* command run and on a record written before the card existed.
|
|
668
|
+
* Additive. */
|
|
669
|
+
card?: ModelCard;
|
|
627
670
|
repo?: string;
|
|
628
671
|
ref?: string;
|
|
629
672
|
pr?: number;
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// arrive in the input rather than from the agent registry.
|
|
31
31
|
|
|
32
32
|
import type { ShipRoundOutcome } from "../runEvents.js";
|
|
33
|
-
import type { Handoff } from "./handoff.js";
|
|
33
|
+
import type { Handoff, HandoffLanded } from "./handoff.js";
|
|
34
34
|
import { progressOf, renderRenewal, renewalDecision, type PushedHeadFact, type RenewalDecision } from "./renewal.js";
|
|
35
35
|
import type { RunStatus } from "../runRecord.js";
|
|
36
36
|
import { normalizeHead, sameCommit } from "../reviewedHead.js";
|
|
@@ -504,8 +504,21 @@ export type PrCheck =
|
|
|
504
504
|
* the base and the head) or `no_base` (the instance names no base to
|
|
505
505
|
* open against, so no create was tried). Absent on a plain check. */
|
|
506
506
|
unrecovered?: "no_commits" | "no_base";
|
|
507
|
+
/** On a plain check: the branch's commits over the base as GitHub
|
|
508
|
+
* compares them, when the bot could read them. Zero beside a handoff
|
|
509
|
+
* naming where the scope landed is the `already_landed` ending
|
|
510
|
+
* (agent-ship item 12); absent, the fact is unknown and never claimed. */
|
|
511
|
+
aheadOfBase?: number;
|
|
512
|
+
}
|
|
513
|
+
| {
|
|
514
|
+
state: "open";
|
|
515
|
+
prNumber: number;
|
|
516
|
+
url: string;
|
|
517
|
+
headSha?: string;
|
|
518
|
+
autoMergeEnabled?: boolean;
|
|
519
|
+
/** The check runs at the head, when the read asked for them (the ending's facts). */
|
|
520
|
+
checks?: CommitChecksFacts;
|
|
507
521
|
}
|
|
508
|
-
| { state: "open"; prNumber: number; url: string; headSha?: string; autoMergeEnabled?: boolean }
|
|
509
522
|
| { state: "merged"; prNumber: number; url: string; sha: string; mergedAt: string };
|
|
510
523
|
|
|
511
524
|
/** What a step answered. Every bot answer carries `at`, the bot's clock — the machine's time. */
|
|
@@ -536,6 +549,11 @@ export type StepReturn =
|
|
|
536
549
|
export type UnitEnding =
|
|
537
550
|
| { kind: "merged"; by: "runner"; pr: PrRef; sha: string; reviewRounds: number }
|
|
538
551
|
| { kind: "merged"; by: "other"; pr: PrRef; sha: string; mergedAt: string; reviewRounds: number }
|
|
552
|
+
/** Round 0 found the unit's scope already on the base (agent-ship item 12):
|
|
553
|
+
* the coding child's handoff names where it landed and the branch has no
|
|
554
|
+
* commits over the base, so there is no pull request to open or review.
|
|
555
|
+
* The unit is done and its dependents start on a base that carries it. */
|
|
556
|
+
| { kind: "already_landed"; landed: HandoffLanded[]; round: RoundRef; runId: string; reviewRounds: number }
|
|
539
557
|
| { kind: "merge_ready"; pr: PrRef; reviewRounds: number }
|
|
540
558
|
| { kind: "merge_refused"; pr: PrRef; reason: string; reviewRounds: number }
|
|
541
559
|
| { kind: "round_cap"; maxRounds: number; reviewRounds: number }
|
|
@@ -1268,15 +1286,26 @@ function settlePrCheck(s: UnitPipelineState, phase: Extract<Phase, { at: "pr-che
|
|
|
1268
1286
|
[roundNote(round, "aborted")],
|
|
1269
1287
|
);
|
|
1270
1288
|
}
|
|
1271
|
-
// Round 0 ended without a pull request: the segment is over with the unit
|
|
1272
|
-
// unfinished, and the grant decides whether the next opens (decision
|
|
1273
|
-
// 0046, Renewal). Progress is read off the child's record — a head pushed
|
|
1274
|
-
// to the unit's branch since the segment started, or a handoff that moved —
|
|
1275
|
-
// never off its words; the decision then asks the grant's count, the cap
|
|
1276
|
-
// and the fit, in that order, and a refusal names the clause. A plain
|
|
1277
|
-
// abort keeps its old shape when nothing was pushed under a grant of zero:
|
|
1278
|
-
// a clarifying question is not a stop to explain.
|
|
1279
1289
|
if (round.index === 0) {
|
|
1290
|
+
// The scope already landed (agent-ship item 12): the child's handoff
|
|
1291
|
+
// names where, and the branch carries no commits over the base — two
|
|
1292
|
+
// facts off the record and GitHub, never the child's prose alone. There
|
|
1293
|
+
// is nothing to open, review or renew: the unit is done. Either fact
|
|
1294
|
+
// missing (a handoff that names no landing, commits on the branch, a
|
|
1295
|
+
// compare the bot could not read) leaves the round-0 ending below.
|
|
1296
|
+
const landed = phase.childHandoff?.landed ?? [];
|
|
1297
|
+
if (landed.length > 0 && pr.aheadOfBase === 0)
|
|
1298
|
+
return end(s, { kind: "already_landed", landed, round, runId: phase.runId, reviewRounds: s.reviewRounds }, [
|
|
1299
|
+
roundNote(round, "completed"),
|
|
1300
|
+
]);
|
|
1301
|
+
// Round 0 ended without a pull request: the segment is over with the unit
|
|
1302
|
+
// unfinished, and the grant decides whether the next opens (decision
|
|
1303
|
+
// 0046, Renewal). Progress is read off the child's record — a head pushed
|
|
1304
|
+
// to the unit's branch since the segment started, or a handoff that moved —
|
|
1305
|
+
// never off its words; the decision then asks the grant's count, the cap
|
|
1306
|
+
// and the fit, in that order, and a refusal names the clause. A plain
|
|
1307
|
+
// abort keeps its old shape when nothing was pushed under a grant of zero:
|
|
1308
|
+
// a clarifying question is not a stop to explain.
|
|
1280
1309
|
const grant = s.input.grant ?? DEFAULT_GRANT;
|
|
1281
1310
|
const session = s.input.session;
|
|
1282
1311
|
const progress = progressOf({
|
|
@@ -1656,6 +1685,30 @@ function splitReport(s: UnitPipelineState): string {
|
|
|
1656
1685
|
export interface MergeReadyFacts {
|
|
1657
1686
|
autoMergeEnabled?: boolean;
|
|
1658
1687
|
merged?: { sha: string; mergedAt: string };
|
|
1688
|
+
/** The check runs at the approved head as the merge door reads them (record 0055). */
|
|
1689
|
+
checks?: CommitChecksFacts;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/** The check runs at one commit: how many, which still run, which failed. */
|
|
1693
|
+
export interface CommitChecksFacts {
|
|
1694
|
+
total: number;
|
|
1695
|
+
pending: string[];
|
|
1696
|
+
failed: string[];
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/** The merge-ready report's headline is a claim about the approved head
|
|
1700
|
+
* (record 0055): a failed check is never called merge-ready, a pending one
|
|
1701
|
+
* is named, green is said, and without the fact the line is unchanged. */
|
|
1702
|
+
function mergeReadyHeadline(rounds: string, url: string, checks: CommitChecksFacts | undefined): string {
|
|
1703
|
+
if (checks === undefined) return `✅ Merge-ready after ${rounds}: ${url}`;
|
|
1704
|
+
if (checks.failed.length > 0) {
|
|
1705
|
+
const pending = checks.pending.length > 0 ? `; pending: ${checks.pending.join(", ")}` : "";
|
|
1706
|
+
return `⚠️ Approved but not merge-ready after ${rounds}: ${url} — CI is red at the approved head: ${checks.failed.join(", ")}${pending}. Fix it and re-review, or rerun a flake; the runner calls a head merge-ready only over green checks.`;
|
|
1707
|
+
}
|
|
1708
|
+
if (checks.pending.length > 0)
|
|
1709
|
+
return `✅ Approved after ${rounds}: ${url} — checks pending at the approved head: ${checks.pending.join(", ")}; merge-ready once they pass.`;
|
|
1710
|
+
if (checks.total === 0) return `✅ Merge-ready after ${rounds}: ${url} — no check reported at the approved head.`;
|
|
1711
|
+
return `✅ Merge-ready after ${rounds}: ${url} — ${checks.total} check${checks.total === 1 ? "" : "s"} green at the approved head.`;
|
|
1659
1712
|
}
|
|
1660
1713
|
|
|
1661
1714
|
export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts): string {
|
|
@@ -1703,9 +1756,16 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1703
1756
|
...(skippedLine ? [skippedLine] : []),
|
|
1704
1757
|
declinedLine,
|
|
1705
1758
|
].join("\n");
|
|
1759
|
+
case "already_landed":
|
|
1760
|
+
// No compare link, no renewal line, no re-issue prompt: there was
|
|
1761
|
+
// nothing to ship, so none of them has a question to answer.
|
|
1762
|
+
return `✅ Already on \`${s.input.base}\`: the unit's scope landed before this attempt — ${e.landed.map((l) => `${l.what} (${l.where})`).join("; ")}. The coding child (run ${e.runId}) found it there and pushed nothing of its own: \`${s.input.unit.branch}\` has no commits over \`${s.input.base}\`, so there is no pull request to open or review. The unit is done and its dependents start on a base that carries it.`;
|
|
1706
1763
|
case "merge_ready":
|
|
1707
1764
|
return [
|
|
1708
|
-
|
|
1765
|
+
// A merge that already happened outranks the checks: there is no head left to gate.
|
|
1766
|
+
facts?.merged
|
|
1767
|
+
? `✅ Merge-ready after ${rounds}: ${e.pr.url}`
|
|
1768
|
+
: mergeReadyHeadline(rounds, e.pr.url, facts?.checks),
|
|
1709
1769
|
verdictLine,
|
|
1710
1770
|
levelLine,
|
|
1711
1771
|
grantLine,
|
|
@@ -1721,9 +1781,17 @@ export function renderUnitReport(s: UnitPipelineState, facts?: MergeReadyFacts):
|
|
|
1721
1781
|
: "Remaining gate: a person's merge — the runner merges only when the instance's `merge` field says runner, and ship never approves.",
|
|
1722
1782
|
].join("\n");
|
|
1723
1783
|
case "merge_refused":
|
|
1784
|
+
// The approved work is on the branch, so the remedy is a person's hand
|
|
1785
|
+
// merge, never a re-run: a seeded plan re-issued afterwards finds the
|
|
1786
|
+
// merged pull request (the pre-check's `merged` by other, or
|
|
1787
|
+
// `already_landed`) and moves on to the dependents. The generated
|
|
1788
|
+
// plan's line already says to re-issue with the PR URL, which takes the
|
|
1789
|
+
// same recognition path.
|
|
1724
1790
|
return join([
|
|
1725
1791
|
`⚠️ The review approved ${e.pr.url} but the runner did not merge it: ${e.reason}. A person decides what becomes of the pull request.`,
|
|
1726
|
-
|
|
1792
|
+
s.input.generated
|
|
1793
|
+
? reissue
|
|
1794
|
+
: `The approved work is on the branch: rebase or fix it, push, and merge it by hand. Then re-issue the plan naming the remaining units — a unit whose pull request has merged is recognized and not run again, and its dependents start from there.`,
|
|
1727
1795
|
]);
|
|
1728
1796
|
case "round_cap":
|
|
1729
1797
|
return join([
|