pi-daddy 0.32.1 → 0.35.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/CHANGELOG.md +83 -0
- package/contracts/ledger-record/v1/governance-event.schema.json +46 -1
- package/dist/advisors/advisor.d.ts +49 -0
- package/dist/advisors/advisor.d.ts.map +1 -0
- package/dist/advisors/advisor.js +76 -0
- package/dist/advisors/advisor.js.map +1 -0
- package/dist/advisors/decider.d.ts +79 -0
- package/dist/advisors/decider.d.ts.map +1 -0
- package/dist/advisors/decider.js +29 -0
- package/dist/advisors/decider.js.map +1 -0
- package/dist/advisors/jev.d.ts +41 -0
- package/dist/advisors/jev.d.ts.map +1 -0
- package/dist/advisors/jev.js +108 -0
- package/dist/advisors/jev.js.map +1 -0
- package/dist/advisors/settings.d.ts +47 -0
- package/dist/advisors/settings.d.ts.map +1 -0
- package/dist/advisors/settings.js +98 -0
- package/dist/advisors/settings.js.map +1 -0
- package/dist/executors/activity-session.d.ts.map +1 -1
- package/dist/executors/activity-session.js +27 -0
- package/dist/executors/activity-session.js.map +1 -1
- package/dist/executors/herdr-stage.d.ts +1 -1
- package/dist/executors/herdr-stage.d.ts.map +1 -1
- package/dist/executors/herdr-stage.js +25 -8
- package/dist/executors/herdr-stage.js.map +1 -1
- package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
- package/dist/governance/ledger-v3-validation.js +1 -0
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/ledger.d.ts +14 -0
- package/dist/governance/ledger.d.ts.map +1 -1
- package/dist/governance/ledger.js +1 -0
- package/dist/governance/ledger.js.map +1 -1
- package/dist/kernel/capabilities.d.ts +1 -1
- package/dist/kernel/capabilities.d.ts.map +1 -1
- package/dist/kernel/capabilities.js +5 -1
- package/dist/kernel/capabilities.js.map +1 -1
- package/dist/kernel/catalog.d.ts.map +1 -1
- package/dist/kernel/catalog.js +6 -0
- package/dist/kernel/catalog.js.map +1 -1
- package/dist/kernel/chain.d.ts +2 -0
- package/dist/kernel/chain.d.ts.map +1 -1
- package/dist/kernel/chain.js.map +1 -1
- package/dist/kernel/context-handoff.d.ts +85 -0
- package/dist/kernel/context-handoff.d.ts.map +1 -0
- package/dist/kernel/context-handoff.js +177 -0
- package/dist/kernel/context-handoff.js.map +1 -0
- package/dist/kernel/delegate-types.d.ts +70 -0
- package/dist/kernel/delegate-types.d.ts.map +1 -1
- package/dist/kernel/delegate-types.js.map +1 -1
- package/dist/kernel/delegate.d.ts.map +1 -1
- package/dist/kernel/delegate.js +50 -2
- package/dist/kernel/delegate.js.map +1 -1
- package/dist/kernel/env-names.d.ts +24 -0
- package/dist/kernel/env-names.d.ts.map +1 -1
- package/dist/kernel/env-names.js +27 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/propagation.d.ts +1 -1
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +14 -2
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/kernel/refusals.d.ts +1 -1
- package/dist/kernel/refusals.d.ts.map +1 -1
- package/dist/kernel/refusals.js +1 -0
- package/dist/kernel/refusals.js.map +1 -1
- package/dist/kernel/resolve.d.ts.map +1 -1
- package/dist/kernel/resolve.js +4 -0
- package/dist/kernel/resolve.js.map +1 -1
- package/dist/kernel/spawn.d.ts +21 -0
- package/dist/kernel/spawn.d.ts.map +1 -1
- package/dist/kernel/spawn.js +8 -1
- package/dist/kernel/spawn.js.map +1 -1
- package/extensions/advisor-session.ts +64 -0
- package/extensions/chain-plan.ts +7 -1
- package/extensions/context-shape.ts +30 -0
- package/extensions/context-staging.ts +208 -0
- package/extensions/delegate-chain.ts +2 -0
- package/extensions/delegation-ledger.ts +2 -0
- package/extensions/delegation.ts +4 -0
- package/extensions/effort-advice.ts +99 -0
- package/extensions/execute-child.ts +8 -0
- package/extensions/grants-command.ts +10 -0
- package/extensions/grants.ts +9 -0
- package/extensions/pruning-advice.ts +88 -0
- package/extensions/run-delegation.ts +64 -3
- package/extensions/session.ts +59 -1
- package/package.json +1 -1
- package/src/advisors/advisor.ts +123 -0
- package/src/advisors/decider.ts +68 -0
- package/src/advisors/jev.ts +130 -0
- package/src/advisors/settings.ts +113 -0
- package/src/executors/activity-session.ts +28 -0
- package/src/executors/herdr-stage.ts +26 -8
- package/src/governance/ledger-v3-validation.ts +1 -0
- package/src/governance/ledger.ts +15 -0
- package/src/kernel/capabilities.ts +5 -1
- package/src/kernel/catalog.ts +6 -0
- package/src/kernel/chain.ts +2 -0
- package/src/kernel/context-handoff.ts +231 -0
- package/src/kernel/delegate-types.ts +62 -0
- package/src/kernel/delegate.ts +56 -2
- package/src/kernel/env-names.ts +27 -0
- package/src/kernel/propagation.ts +16 -1
- package/src/kernel/refusals.ts +1 -0
- package/src/kernel/resolve.ts +4 -0
- package/src/kernel/spawn.ts +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,89 @@ the record of how the package got here and are worth keeping; they are not worth
|
|
|
12
12
|
> the record of how the package arrived at what it does, and because the reasoning behind each one is
|
|
13
13
|
> usually the clearest statement of why the current behaviour is what it is.
|
|
14
14
|
|
|
15
|
+
## 0.35.0 — the first two decision points, and only the environment can enable an advisor
|
|
16
|
+
|
|
17
|
+
**An advisor now fills two blanks.** When a `delegate` call names no `thinking` level, an enabled advisor is asked
|
|
18
|
+
to choose one from the levels the CHILD's model reports it supports. An explicit level is never overruled, and with
|
|
19
|
+
no advisor, no answer or a timeout the blank stays blank and the child is spawned exactly as before. The task text
|
|
20
|
+
is sent to the advisor so it has something to judge; it is still never recorded.
|
|
21
|
+
|
|
22
|
+
**A `pruned` context handoff can now ask which turns to carry.** The mechanical rule runs first and decides what is
|
|
23
|
+
eligible; the advisor is then shown at most twelve of those turns, truncated, and may only NARROW the set — an id
|
|
24
|
+
it invents or one the rule dropped is ignored, and an incomplete answer is discarded whole. The selection is asked
|
|
25
|
+
only after a plan says the handoff survived the ceiling, the grant and the gate, so a delegation the grant refuses
|
|
26
|
+
never ships session turns. With no advisor or no usable answer, the mechanical selection stands unchanged.
|
|
27
|
+
|
|
28
|
+
**This sends more than the effort point does.** A `pruned` handoff sends the operator's own session turns, not just
|
|
29
|
+
the task, to a third party. That is the sharpest egress in the package and it is why an advisor is off by default,
|
|
30
|
+
enabled only from the environment, and reported by `/grants`.
|
|
31
|
+
|
|
32
|
+
**Security fix over 0.34.0.**
|
|
33
|
+
|
|
34
|
+
0.34.0 read the advisor's enable switch from `.pi/pi-daddy/settings.json`. That file is writable by any child
|
|
35
|
+
holding `tool:write`, and this package keeps the grant outside the workspace for exactly that reason: a ceiling a
|
|
36
|
+
governed child can rewrite is not a ceiling. The same argument applies here one step sideways — a child could have
|
|
37
|
+
flipped the switch and made the operator's next session send its own description to a third party.
|
|
38
|
+
|
|
39
|
+
**What to do.** Enabling an advisor is now `PI_DADDY_ADVISOR=jev` alongside `PI_DADDY_ADVISOR_KEY`, with
|
|
40
|
+
`PI_DADDY_ADVISOR_MODEL` to override the model; all three are stripped from a child spawned as a subprocess. A
|
|
41
|
+
Herdr pane inherits the daemon's environment, so a daemon started from a shell exporting them still hands them to
|
|
42
|
+
pane children — stated rather than implied, and not yet closed.
|
|
43
|
+
|
|
44
|
+
A project's `advisor` block in `settings.json` may turn an advisor off for that project and shorten its timeout. It
|
|
45
|
+
can no longer turn one on, choose its model, or lengthen its bound: a model is a destination and a longer bound is
|
|
46
|
+
not a narrowing, and that file is writable by any child holding `tool:write`. Anything that is not exactly `true`
|
|
47
|
+
on `enabled` disables. So a settings file that relied on `enabled: true` will find the advisor off until the
|
|
48
|
+
environment variable is set, and one that set `model` will be refused with a message naming the variable to use.
|
|
49
|
+
|
|
50
|
+
## 0.34.0 — an advisors layer, off by default (ADR-0077)
|
|
51
|
+
|
|
52
|
+
`src/advisors/` holds a `Decider` that answers typed questions — `noul` (a boolean), `choice` (one of the options
|
|
53
|
+
the caller already had) and `score` (a level from the caller's own list) — each with a probability. It may select,
|
|
54
|
+
rank, annotate or propose, and it can never widen a grant, satisfy a gate or replace a human's answer. That is
|
|
55
|
+
enforced rather than promised: no type in the layer names a capability or a refusal code, and no kernel or
|
|
56
|
+
governance module imports it, both checked by tests.
|
|
57
|
+
|
|
58
|
+
Off by default. An advisor runs only when `.pi/pi-daddy/settings.json` has an `advisor` block saying so and
|
|
59
|
+
`PI_DADDY_ADVISOR_KEY` is set; malformed configuration disables it and names the field rather than failing either
|
|
60
|
+
silently or open. Every use writes an `advice` record to the ledger, including the uses that produced nothing, and
|
|
61
|
+
that record never contains the state the caller composed. Degradation is always "no advice": disabled, no key, a
|
|
62
|
+
two-second timeout, a transport error or an unrecognised response all return the same nothing, so a caller written
|
|
63
|
+
against the null decider behaves identically with an advisor present.
|
|
64
|
+
|
|
65
|
+
The first adapter is TypeSafe's Jev through OpenRouter's Decisions endpoint (`typesafe/jev-1.13`). The request shape
|
|
66
|
+
is the documented one. The response shape is **not confirmed against a live call** — OpenRouter describes the
|
|
67
|
+
`answers` object without showing it — so the parser accepts what the documentation describes and treats anything
|
|
68
|
+
else, including a choice that was never offered, as no advice. No decision point uses an advisor yet.
|
|
69
|
+
|
|
70
|
+
There is no dashboard toggle: the dashboard is a read-only renderer that never affects enforcement, so enabling an
|
|
71
|
+
advisor is an operator edit to the reviewable settings file.
|
|
72
|
+
|
|
73
|
+
## 0.33.0 — a child can be given context, and the giving attenuates (ADR-0078)
|
|
74
|
+
|
|
75
|
+
`delegate`, `delegate_all` and each fan-out child accept a `context` parameter naming how much of the parent's own
|
|
76
|
+
session crosses: `none` (the default, and what every child got before), `files`, `pruned`, `summary` or `fork`. Each
|
|
77
|
+
mode is a `context:<mode>` capability in a new namespace, so it is intersected with the parent's grant and the
|
|
78
|
+
definition's `allowed-tools` ceiling exactly like a tool, appears in `/grants` and in the ledger's effective set, and
|
|
79
|
+
cannot be widened by a child. The modes are ordered and each subsumes the weaker ones.
|
|
80
|
+
|
|
81
|
+
`context:fork` is **gated by default** beside `tool:bash`: it is the one mode that can carry content an untrusted
|
|
82
|
+
repository put in front of the parent into a fresh child. What crosses arrives in a `<<<PARENT-CONTEXT …>>>` fence
|
|
83
|
+
marked as data, distinct from the chain handoff's fence, capped at 32 KiB with anything dropped said inside the
|
|
84
|
+
fence. Paths named for `files` are confined to the session's working directory. The capability-decision record gains
|
|
85
|
+
`handoff`, naming the mode the child received, how many sections and bytes crossed, and for `pruned` which rule ran
|
|
86
|
+
and how many turns it kept.
|
|
87
|
+
|
|
88
|
+
Nothing changes for a caller that passes no `context`: the default is `none`, which adds no capability and crosses
|
|
89
|
+
nothing. `pruned`'s selection rule is deterministic but its recall is unmeasured, so it is not a default.
|
|
90
|
+
|
|
91
|
+
A `delegate_chain` step takes the same parameter. A chain is planned as one unit, so a step's handoff is capped by
|
|
92
|
+
that step's own definition and any gate it raises is answered before the first step runs.
|
|
93
|
+
|
|
94
|
+
**Breaking for consumers of the package API:** `splitSystemPrompt` now returns `systemPrompts: string[]` instead of
|
|
95
|
+
`systemPrompt?: string`, because a child can carry more than one appended system prompt and only the first was being
|
|
96
|
+
staged for the Herdr executor.
|
|
97
|
+
|
|
15
98
|
## 0.32.1 — a doubled namespace in `allowed-tools` is explained, not just reported
|
|
16
99
|
|
|
17
100
|
An `allowed-tools` entry written with a capitalised namespace, such as `Tool:Read`, misses the lower-case prefix test,
|
|
@@ -185,7 +185,8 @@
|
|
|
185
185
|
"WORKSPACE_NOT_AUTHORIZED",
|
|
186
186
|
"WORKSPACE_WRITE_CONFLICT",
|
|
187
187
|
"WORKSPACE_LEASE_STALE",
|
|
188
|
-
"LEDGER_DAMAGED"
|
|
188
|
+
"LEDGER_DAMAGED",
|
|
189
|
+
"CONTEXT_REQUEST_INVALID"
|
|
189
190
|
]
|
|
190
191
|
},
|
|
191
192
|
"refusal": {
|
|
@@ -401,6 +402,50 @@
|
|
|
401
402
|
"definitionDigest": {
|
|
402
403
|
"$ref": "#/$defs/definitionDigest"
|
|
403
404
|
},
|
|
405
|
+
"handoff": {
|
|
406
|
+
"type": "object",
|
|
407
|
+
"description": "the context handoff this child received (ADR-0078); absent means nothing crossed",
|
|
408
|
+
"additionalProperties": false,
|
|
409
|
+
"required": [
|
|
410
|
+
"mode",
|
|
411
|
+
"sections",
|
|
412
|
+
"bytes",
|
|
413
|
+
"truncatedBytes"
|
|
414
|
+
],
|
|
415
|
+
"properties": {
|
|
416
|
+
"mode": {
|
|
417
|
+
"enum": [
|
|
418
|
+
"files",
|
|
419
|
+
"pruned",
|
|
420
|
+
"summary",
|
|
421
|
+
"fork"
|
|
422
|
+
]
|
|
423
|
+
},
|
|
424
|
+
"sections": {
|
|
425
|
+
"type": "integer",
|
|
426
|
+
"minimum": 0
|
|
427
|
+
},
|
|
428
|
+
"bytes": {
|
|
429
|
+
"type": "integer",
|
|
430
|
+
"minimum": 0
|
|
431
|
+
},
|
|
432
|
+
"truncatedBytes": {
|
|
433
|
+
"type": "integer",
|
|
434
|
+
"minimum": 0
|
|
435
|
+
},
|
|
436
|
+
"keptTurns": {
|
|
437
|
+
"type": "integer",
|
|
438
|
+
"minimum": 0
|
|
439
|
+
},
|
|
440
|
+
"droppedTurns": {
|
|
441
|
+
"type": "integer",
|
|
442
|
+
"minimum": 0
|
|
443
|
+
},
|
|
444
|
+
"rule": {
|
|
445
|
+
"type": "string"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
},
|
|
404
449
|
"executor": {
|
|
405
450
|
"type": "string",
|
|
406
451
|
"enum": [
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wrapper that makes "every use is recorded" structural rather than a rule (ADR-0077).
|
|
3
|
+
*
|
|
4
|
+
* A caller cannot reach a `Decider` directly through this layer's public surface: it asks an `Advisor`, and asking
|
|
5
|
+
* always produces an `advice` record — including when the answer was "no advice", which is the case a reviewer most
|
|
6
|
+
* needs to see, because an advisor that silently stops answering would otherwise look exactly like one nobody used.
|
|
7
|
+
*
|
|
8
|
+
* **What is recorded, and what is not.** The record names the purpose, the decider, the question keys, the answers
|
|
9
|
+
* and how long it took. It does NOT contain the state. A caller composes that state from its own context, which can
|
|
10
|
+
* include task text, file contents and a repository's private material; the ledger has never stored a task
|
|
11
|
+
* (ADR-0021) and an advisor must not become the way it starts. The question keys are the caller's own constants,
|
|
12
|
+
* so they name the decision without describing the situation.
|
|
13
|
+
*/
|
|
14
|
+
import type { Advice, AdviceRequest, Decider } from "./decider.ts";
|
|
15
|
+
/** Two seconds. An advisor is on the path of a decision a human is waiting for; it is not worth more than that. */
|
|
16
|
+
export declare const DEFAULT_ADVICE_TIMEOUT_MS = 2000;
|
|
17
|
+
export interface AdviceRecord {
|
|
18
|
+
/** Which decision this advice was for, from the caller's own closed list. */
|
|
19
|
+
purpose: string;
|
|
20
|
+
decider: string;
|
|
21
|
+
/** Question keys only — never the state, and never a question's free text. */
|
|
22
|
+
questions: string[];
|
|
23
|
+
answered: boolean;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
/** Present only when advice came back. */
|
|
26
|
+
answers?: Readonly<Record<string, {
|
|
27
|
+
value: string | number | boolean;
|
|
28
|
+
confidence?: number;
|
|
29
|
+
}>>;
|
|
30
|
+
model?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Why there is no advice. `declined` means the advisor answered with nothing; `error` means it could not be
|
|
33
|
+
* reached or its response was unrecognised; `cancelled` means the CALLER went away, which is not the advisor's
|
|
34
|
+
* failure and must not read as one.
|
|
35
|
+
*/
|
|
36
|
+
outcome: "answered" | "disabled" | "timeout" | "error" | "declined" | "cancelled";
|
|
37
|
+
}
|
|
38
|
+
export interface Advisor {
|
|
39
|
+
ask(purpose: string, request: AdviceRequest, signal?: AbortSignal): Promise<Advice | null>;
|
|
40
|
+
}
|
|
41
|
+
export declare function createAdvisor(input: {
|
|
42
|
+
decider: Decider;
|
|
43
|
+
/** Where the record goes. Injected so this layer does no I/O and governance does not import it. */
|
|
44
|
+
record: (entry: AdviceRecord) => void | Promise<void>;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
/** Absent or false means the null decider is used whatever `decider` says. */
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
}): Advisor;
|
|
49
|
+
//# sourceMappingURL=advisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advisor.d.ts","sourceRoot":"","sources":["../../src/advisors/advisor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEnE,mHAAmH;AACnH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAC9F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,WAAW,CAAC;CACnF;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC5F;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,mGAAmG;IACnG,MAAM,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CA+DV"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Two seconds. An advisor is on the path of a decision a human is waiting for; it is not worth more than that. */
|
|
2
|
+
export const DEFAULT_ADVICE_TIMEOUT_MS = 2000;
|
|
3
|
+
export function createAdvisor(input) {
|
|
4
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_ADVICE_TIMEOUT_MS;
|
|
5
|
+
return {
|
|
6
|
+
async ask(purpose, request, signal) {
|
|
7
|
+
const started = Date.now();
|
|
8
|
+
const base = { purpose, decider: input.decider.name, questions: Object.keys(request.questions) };
|
|
9
|
+
const write = async (entry) => {
|
|
10
|
+
try {
|
|
11
|
+
await input.record(entry);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Recording is an observation of a decision that has already been taken. Failing to write it must not
|
|
15
|
+
// change what the caller does, for `execute-child`'s reason: an audit failure that discards the work is
|
|
16
|
+
// worse than one that is merely missing.
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
if (input.enabled !== true) {
|
|
20
|
+
await write({ ...base, decider: "none", answered: false, durationMs: 0, outcome: "disabled" });
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const timer = new AbortController();
|
|
24
|
+
const cancel = setTimeout(() => timer.abort(), timeoutMs);
|
|
25
|
+
const linked = signal ? AbortSignal.any([signal, timer.signal]) : timer.signal;
|
|
26
|
+
try {
|
|
27
|
+
// RACED, not merely signalled. A decider that ignores its signal would otherwise run as long as it liked
|
|
28
|
+
// and then be recorded as a timeout — measured at fifty times the configured bound. The `Decider` contract
|
|
29
|
+
// cannot make an implementation honour an abort, so the bound is enforced on this side of it.
|
|
30
|
+
const advice = await Promise.race([
|
|
31
|
+
input.decider.decide(request, linked),
|
|
32
|
+
new Promise((settle) => linked.addEventListener("abort", () => settle(null), { once: true })),
|
|
33
|
+
]);
|
|
34
|
+
const durationMs = Date.now() - started;
|
|
35
|
+
if (!advice) {
|
|
36
|
+
await write({ ...base, answered: false, durationMs, outcome: outcomeFor(timer.signal, signal, "declined") });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
await write({
|
|
40
|
+
...base,
|
|
41
|
+
answered: true,
|
|
42
|
+
durationMs,
|
|
43
|
+
outcome: "answered",
|
|
44
|
+
answers: Object.fromEntries(Object.entries(advice.answers).map(([key, answer]) => [
|
|
45
|
+
key,
|
|
46
|
+
{ value: answer.value, ...(answer.confidence === undefined ? {} : { confidence: answer.confidence }) },
|
|
47
|
+
])),
|
|
48
|
+
...(advice.model ? { model: advice.model } : {}),
|
|
49
|
+
});
|
|
50
|
+
return advice;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Including an abort. A caller asked for advice and is getting none; it proceeds exactly as it would have.
|
|
54
|
+
await write({
|
|
55
|
+
...base,
|
|
56
|
+
answered: false,
|
|
57
|
+
durationMs: Date.now() - started,
|
|
58
|
+
outcome: outcomeFor(timer.signal, signal, "error"),
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(cancel);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** The bound fired, the caller went away, or neither — three different facts a reviewer needs to tell apart. */
|
|
69
|
+
function outcomeFor(timer, caller, otherwise) {
|
|
70
|
+
if (timer.aborted && !caller?.aborted)
|
|
71
|
+
return "timeout";
|
|
72
|
+
if (caller?.aborted)
|
|
73
|
+
return "cancelled";
|
|
74
|
+
return otherwise;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=advisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advisor.js","sourceRoot":"","sources":["../../src/advisors/advisor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An advisor is a thing that answers a typed question. It is never a thing that decides (ADR-0077).
|
|
3
|
+
*
|
|
4
|
+
* The boundary is the whole design, and it is structural rather than remembered: **no type in this layer names a
|
|
5
|
+
* `Capability` or a refusal code**, and no function in `kernel/` or `governance/` accepts an advisor's result. An
|
|
6
|
+
* advisor may select among options the caller already had, rank them, annotate them, or propose one. It can never
|
|
7
|
+
* widen an `effective` set, satisfy a gate, or stand in for a human's answer — not because it is asked not to, but
|
|
8
|
+
* because nothing on those paths can receive what it returns. `test/advisors.test.ts` forces that.
|
|
9
|
+
*
|
|
10
|
+
* Non-generative on purpose. The first advisor is a classifier that returns a typed answer with a probability, not
|
|
11
|
+
* prose, which is what makes an advisor auditable: "it chose `b` at 0.91" is a fact a reviewer can disagree with,
|
|
12
|
+
* where a paragraph of reasoning is not.
|
|
13
|
+
*
|
|
14
|
+
* Degradation is "no advice", never a guess. Every path that cannot produce an answer — disabled, missing key,
|
|
15
|
+
* timeout, transport error, a response shape we do not recognise — returns `null`, and the caller does what it
|
|
16
|
+
* would have done without an advisor at all. That is why a caller must be written to work with `nullDecider`
|
|
17
|
+
* first, and why `nullDecider` is the default.
|
|
18
|
+
*/
|
|
19
|
+
/** A question, in the three shapes the first advisor understands. */
|
|
20
|
+
export type Question = {
|
|
21
|
+
kind: "noul";
|
|
22
|
+
instructions: string;
|
|
23
|
+
whenTrue: string;
|
|
24
|
+
whenFalse: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "choice";
|
|
27
|
+
instructions: string;
|
|
28
|
+
options: Readonly<Record<string, string>>;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "score";
|
|
31
|
+
instructions: string;
|
|
32
|
+
levels: readonly string[];
|
|
33
|
+
};
|
|
34
|
+
/** One typed answer. `confidence` is absent when the transport did not report one; it is never invented. */
|
|
35
|
+
export type Answer = {
|
|
36
|
+
kind: "noul";
|
|
37
|
+
value: boolean;
|
|
38
|
+
confidence?: number;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "choice";
|
|
41
|
+
value: string;
|
|
42
|
+
confidence?: number;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "score";
|
|
45
|
+
value: number;
|
|
46
|
+
level: string;
|
|
47
|
+
confidence?: number;
|
|
48
|
+
};
|
|
49
|
+
export interface AdviceRequest {
|
|
50
|
+
/**
|
|
51
|
+
* What the advisor is told about the situation, composed by the caller.
|
|
52
|
+
*
|
|
53
|
+
* **Sent, never recorded.** The task is never STORED (ADR-0021) and that still holds — `createAdvisor` writes the
|
|
54
|
+
* question keys and the answers and never this object. But an advisor cannot judge a task it cannot see, so a
|
|
55
|
+
* caller that needs one judged does send it, and the operator's consent for that is the advisor being off by
|
|
56
|
+
* default. An earlier draft of this paragraph said the raw task "must not be shipped to a third party either",
|
|
57
|
+
* which the first decision point then did; the rule that survived review is the narrower and true one.
|
|
58
|
+
*/
|
|
59
|
+
state: Readonly<Record<string, unknown>>;
|
|
60
|
+
questions: Readonly<Record<string, Question>>;
|
|
61
|
+
}
|
|
62
|
+
export interface Advice {
|
|
63
|
+
answers: Readonly<Record<string, Answer>>;
|
|
64
|
+
/** What actually answered, as the transport reported it — a dated model id, not the one we asked for. */
|
|
65
|
+
model?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface Decider {
|
|
68
|
+
/** Recorded in the ledger so a reviewer can tell which advisor a decision was taken beside. */
|
|
69
|
+
readonly name: string;
|
|
70
|
+
decide(request: AdviceRequest, signal?: AbortSignal): Promise<Advice | null>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The default, and the one every caller must work correctly with.
|
|
74
|
+
*
|
|
75
|
+
* Not a placeholder: it is how advisors stay optional. A caller that behaves differently under `nullDecider` than
|
|
76
|
+
* under no advisor at all has made advice load-bearing, which is the one thing ADR-0077 forbids.
|
|
77
|
+
*/
|
|
78
|
+
export declare const nullDecider: Decider;
|
|
79
|
+
//# sourceMappingURL=decider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decider.d.ts","sourceRoot":"","sources":["../../src/advisors/decider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,qEAAqE;AACrE,MAAM,MAAM,QAAQ,GAChB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,GACnF;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEvE,4GAA4G;AAC5G,MAAM,MAAM,MAAM,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAEtD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE,MAAM,WAAW,aAAa;IAC5B;;;;;;;;OAQG;IACH,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACzC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,yGAAyG;IACzG,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,OAAO;IACtB,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9E;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,OAGzB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An advisor is a thing that answers a typed question. It is never a thing that decides (ADR-0077).
|
|
3
|
+
*
|
|
4
|
+
* The boundary is the whole design, and it is structural rather than remembered: **no type in this layer names a
|
|
5
|
+
* `Capability` or a refusal code**, and no function in `kernel/` or `governance/` accepts an advisor's result. An
|
|
6
|
+
* advisor may select among options the caller already had, rank them, annotate them, or propose one. It can never
|
|
7
|
+
* widen an `effective` set, satisfy a gate, or stand in for a human's answer — not because it is asked not to, but
|
|
8
|
+
* because nothing on those paths can receive what it returns. `test/advisors.test.ts` forces that.
|
|
9
|
+
*
|
|
10
|
+
* Non-generative on purpose. The first advisor is a classifier that returns a typed answer with a probability, not
|
|
11
|
+
* prose, which is what makes an advisor auditable: "it chose `b` at 0.91" is a fact a reviewer can disagree with,
|
|
12
|
+
* where a paragraph of reasoning is not.
|
|
13
|
+
*
|
|
14
|
+
* Degradation is "no advice", never a guess. Every path that cannot produce an answer — disabled, missing key,
|
|
15
|
+
* timeout, transport error, a response shape we do not recognise — returns `null`, and the caller does what it
|
|
16
|
+
* would have done without an advisor at all. That is why a caller must be written to work with `nullDecider`
|
|
17
|
+
* first, and why `nullDecider` is the default.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The default, and the one every caller must work correctly with.
|
|
21
|
+
*
|
|
22
|
+
* Not a placeholder: it is how advisors stay optional. A caller that behaves differently under `nullDecider` than
|
|
23
|
+
* under no advisor at all has made advice load-bearing, which is the one thing ADR-0077 forbids.
|
|
24
|
+
*/
|
|
25
|
+
export const nullDecider = {
|
|
26
|
+
name: "none",
|
|
27
|
+
decide: async () => null,
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=decider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decider.js","sourceRoot":"","sources":["../../src/advisors/decider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAyCH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAY;IAClC,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI;CACzB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jev, through OpenRouter's Decisions endpoint — the first advisor adapter (ADR-0077).
|
|
3
|
+
*
|
|
4
|
+
* **What is verified and what is not.** The REQUEST shape below is taken from OpenRouter's own SDK reference for
|
|
5
|
+
* `POST /api/alpha/decisions`: `{model, state, questions}`, where each question is `noul` with `criteria.true` and
|
|
6
|
+
* `criteria.false`, `choice` with a `criteria` map of option to description, or `score` with a `criteria` array of
|
|
7
|
+
* level descriptions. That much is documented. The RESPONSE is described there only as an `answers` object beside
|
|
8
|
+
* `id`, `model`, `provider` and `usage`, with probabilities and confidence mentioned but never shown, and the one
|
|
9
|
+
* public guide to this endpoint says plainly that it has not run paid calls either. **So no shape below has been
|
|
10
|
+
* confirmed against a live response.** The parser therefore accepts what the documentation describes, tolerates the
|
|
11
|
+
* obvious variants, and returns `null` for anything else rather than guessing — which is the same thing it does when
|
|
12
|
+
* the endpoint is down. A live check is the `PI_DADDY_IT_JEV=1` tier, and until somebody runs it this adapter's
|
|
13
|
+
* response handling is a reading of documentation, not a measurement.
|
|
14
|
+
*
|
|
15
|
+
* Nothing here can widen anything: it returns `Advice`, and no kernel or governance function accepts one.
|
|
16
|
+
*/
|
|
17
|
+
import type { Advice, AdviceRequest, Answer, Decider, Question } from "./decider.ts";
|
|
18
|
+
export declare const JEV_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
|
|
19
|
+
export declare const JEV_MODEL = "typesafe/jev-1.13";
|
|
20
|
+
/** The wire form of one question, exactly as OpenRouter's reference documents it. */
|
|
21
|
+
export declare function wireQuestion(question: Question): Record<string, unknown>;
|
|
22
|
+
export declare function wireRequest(request: AdviceRequest, model: string): Record<string, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* Read one answer out of a response, or nothing.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately generous about WHERE the value and the probability sit, because the documentation names the fields
|
|
27
|
+
* without showing them, and strict about WHAT they are: a `choice` answer must be one of the options that were
|
|
28
|
+
* asked about, and a `score` must be an index into the levels. An answer outside the question's own vocabulary is
|
|
29
|
+
* not a low-confidence answer, it is a response we did not understand, and the honest reading of that is no advice.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseAnswer(question: Question, raw: unknown): Answer | undefined;
|
|
32
|
+
export declare function parseAdvice(request: AdviceRequest, body: unknown): Advice | null;
|
|
33
|
+
export interface JevConfig {
|
|
34
|
+
apiKey: string;
|
|
35
|
+
model?: string;
|
|
36
|
+
endpoint?: string;
|
|
37
|
+
/** Injected so the adapter is testable without a network, and so nothing here reaches for a global. */
|
|
38
|
+
fetch?: typeof globalThis.fetch;
|
|
39
|
+
}
|
|
40
|
+
export declare function jevDecider(config: JevConfig): Decider;
|
|
41
|
+
//# sourceMappingURL=jev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jev.d.ts","sourceRoot":"","sources":["../../src/advisors/jev.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAErF,eAAO,MAAM,YAAY,8CAA8C,CAAC;AACxE,eAAO,MAAM,SAAS,sBAAsB,CAAC;AAE7C,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUxE;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAM1F;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CA8BhF;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAchF;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uGAAuG;IACvG,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAuBrD"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export const JEV_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
|
|
2
|
+
export const JEV_MODEL = "typesafe/jev-1.13";
|
|
3
|
+
/** The wire form of one question, exactly as OpenRouter's reference documents it. */
|
|
4
|
+
export function wireQuestion(question) {
|
|
5
|
+
if (question.kind === "noul")
|
|
6
|
+
return {
|
|
7
|
+
type: "noul",
|
|
8
|
+
instructions: question.instructions,
|
|
9
|
+
criteria: { true: question.whenTrue, false: question.whenFalse },
|
|
10
|
+
};
|
|
11
|
+
if (question.kind === "choice")
|
|
12
|
+
return { type: "choice", instructions: question.instructions, criteria: { ...question.options } };
|
|
13
|
+
return { type: "score", instructions: question.instructions, criteria: [...question.levels] };
|
|
14
|
+
}
|
|
15
|
+
export function wireRequest(request, model) {
|
|
16
|
+
return {
|
|
17
|
+
model,
|
|
18
|
+
state: request.state,
|
|
19
|
+
questions: Object.fromEntries(Object.entries(request.questions).map(([key, q]) => [key, wireQuestion(q)])),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read one answer out of a response, or nothing.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately generous about WHERE the value and the probability sit, because the documentation names the fields
|
|
26
|
+
* without showing them, and strict about WHAT they are: a `choice` answer must be one of the options that were
|
|
27
|
+
* asked about, and a `score` must be an index into the levels. An answer outside the question's own vocabulary is
|
|
28
|
+
* not a low-confidence answer, it is a response we did not understand, and the honest reading of that is no advice.
|
|
29
|
+
*/
|
|
30
|
+
export function parseAnswer(question, raw) {
|
|
31
|
+
if (raw === null || raw === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const object = typeof raw === "object" && !Array.isArray(raw) ? raw : undefined;
|
|
34
|
+
const value = object ? (object.value ?? object.answer ?? object.choice ?? object.result) : raw;
|
|
35
|
+
const confidenceRaw = object ? (object.confidence ?? object.probability ?? object.p) : undefined;
|
|
36
|
+
const confidence = typeof confidenceRaw === "number" && Number.isFinite(confidenceRaw) ? confidenceRaw : undefined;
|
|
37
|
+
const withConfidence = (answer) => (confidence === undefined ? answer : { ...answer, confidence });
|
|
38
|
+
if (question.kind === "noul") {
|
|
39
|
+
if (typeof value !== "boolean")
|
|
40
|
+
return undefined;
|
|
41
|
+
return withConfidence({ kind: "noul", value });
|
|
42
|
+
}
|
|
43
|
+
if (question.kind === "choice") {
|
|
44
|
+
if (typeof value !== "string" || !Object.hasOwn(question.options, value))
|
|
45
|
+
return undefined;
|
|
46
|
+
return withConfidence({ kind: "choice", value });
|
|
47
|
+
}
|
|
48
|
+
// ONE reading: a score is a 0-based index into the `criteria` array that was asked about, and `level` carries the
|
|
49
|
+
// string so a caller never indexes the number itself.
|
|
50
|
+
//
|
|
51
|
+
// The first draft accepted both a 0-based and a 1-based reading "because the documentation shows neither", which
|
|
52
|
+
// pushed the ambiguity onto the caller and into the ledger: with three levels, 1 and 2 were valid under both, so
|
|
53
|
+
// `levels[value]` could read "high" where the model meant "mid". An advisor exists to remove that guess, not to
|
|
54
|
+
// relocate it. **The 0-based reading is an assumption** — it indexes the documented array form — and it is
|
|
55
|
+
// unverified for the same reason everything else about the response is: no live call has been made. If Jev is
|
|
56
|
+
// 1-based, its top level falls outside the array and the whole answer is refused as unrecognised, which is the
|
|
57
|
+
// loud failure rather than a silently shifted one, and the `PI_DADDY_IT_JEV=1` tier is what would show it.
|
|
58
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
59
|
+
return undefined;
|
|
60
|
+
if (value < 0 || value >= question.levels.length)
|
|
61
|
+
return undefined;
|
|
62
|
+
return withConfidence({ kind: "score", value, level: question.levels[value] });
|
|
63
|
+
}
|
|
64
|
+
export function parseAdvice(request, body) {
|
|
65
|
+
if (typeof body !== "object" || body === null)
|
|
66
|
+
return null;
|
|
67
|
+
const envelope = body;
|
|
68
|
+
const raw = envelope.answers;
|
|
69
|
+
if (typeof raw !== "object" || raw === null)
|
|
70
|
+
return null;
|
|
71
|
+
const answers = {};
|
|
72
|
+
for (const [key, question] of Object.entries(request.questions)) {
|
|
73
|
+
const parsed = parseAnswer(question, raw[key]);
|
|
74
|
+
// Every question or none: a caller that asked two questions and silently received one would have to guess which
|
|
75
|
+
// of its branches the missing answer belonged to, and guessing is what an advisor exists to remove.
|
|
76
|
+
if (!parsed)
|
|
77
|
+
return null;
|
|
78
|
+
answers[key] = parsed;
|
|
79
|
+
}
|
|
80
|
+
return { answers, ...(typeof envelope.model === "string" ? { model: envelope.model } : {}) };
|
|
81
|
+
}
|
|
82
|
+
export function jevDecider(config) {
|
|
83
|
+
return {
|
|
84
|
+
name: "jev",
|
|
85
|
+
async decide(request, signal) {
|
|
86
|
+
const send = config.fetch ?? globalThis.fetch;
|
|
87
|
+
const response = await send(config.endpoint ?? JEV_ENDPOINT, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey}` },
|
|
90
|
+
body: JSON.stringify(wireRequest(request, config.model ?? JEV_MODEL)),
|
|
91
|
+
...(signal ? { signal } : {}),
|
|
92
|
+
});
|
|
93
|
+
// A dead endpoint and an advisor with nothing to say must not read alike in the ledger: the whole reason for
|
|
94
|
+
// recording the nothing-cases is that an advisor which quietly stopped answering should not look like one
|
|
95
|
+
// nobody called. Thrown, so `createAdvisor` records `error` rather than `declined`; it catches, so nothing
|
|
96
|
+
// reaches the caller but `null` either way.
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
throw new Error(`advisor endpoint returned ${response.status}`);
|
|
99
|
+
try {
|
|
100
|
+
return parseAdvice(request, await response.json());
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=jev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jev.js","sourceRoot":"","sources":["../../src/advisors/jev.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether an advisor is on, and which one (ADR-0077).
|
|
3
|
+
*
|
|
4
|
+
* **Default off, and only the environment can turn it on.** An advisor sends a description of the caller's
|
|
5
|
+
* situation to a third party, so enabling one is `PI_DADDY_ADVISOR=jev` plus a key — both outside the workspace,
|
|
6
|
+
* both stripped from every child.
|
|
7
|
+
*
|
|
8
|
+
* 0.34.0 read the enable from `.pi/pi-daddy/settings.json` and that was wrong for the reason `grant-store.ts`
|
|
9
|
+
* states about the same file: it is writable by any child holding `tool:write`, so it is "the reviewable record of
|
|
10
|
+
* the decision, not the thing the enforcer reads". A grant lives outside the workspace precisely so a child cannot
|
|
11
|
+
* widen the next session's ceiling; an advisor switch a child could flip would make the operator's next session
|
|
12
|
+
* ship its own description to a third party, which is the same self-defeating shape. The settings block may still
|
|
13
|
+
* NARROW — a shorter timeout, or `enabled: false` to turn an advisor off for one project — and can never turn one
|
|
14
|
+
* on, choose its model, or lengthen its bound. A model is a destination rather than a narrowing, so `model` in the
|
|
15
|
+
* block is refused with a message naming `PI_DADDY_ADVISOR_MODEL`, and a timeout is clamped to the default rather
|
|
16
|
+
* than trusted. Malformed configuration disables the advisor and says so: a typo must not be a way to enable anything.
|
|
17
|
+
*
|
|
18
|
+
* **Not a dashboard toggle**, which is what the programme originally sketched. The dashboard is a read-only
|
|
19
|
+
* renderer in a separate process that "never affects enforcement" (ADR-0036), and a control there that wrote to
|
|
20
|
+
* settings would be the first thing it ever wrote. Turning an advisor on is an operator decision that belongs in
|
|
21
|
+
* the reviewable file; `/grants` reports what is in force. That is a deliberate departure from the roadmap line.
|
|
22
|
+
*
|
|
23
|
+
* The key is never in the settings file either, because that file is committed and an API key must not be.
|
|
24
|
+
*/
|
|
25
|
+
export { ENV_ADVISOR_KEY as ADVISOR_KEY_ENV } from "../kernel/env-names.ts";
|
|
26
|
+
export { ENV_ADVISOR_MODEL } from "../kernel/env-names.ts";
|
|
27
|
+
export { ENV_ADVISOR } from "../kernel/env-names.ts";
|
|
28
|
+
export interface AdvisorSettings {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
/** The only decider this release knows besides the null one. */
|
|
31
|
+
decider: "none" | "jev";
|
|
32
|
+
/** Overrides the adapter's pinned model id; absent means the adapter's own default. */
|
|
33
|
+
model?: string;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
/** Why an advisor is off when the settings asked for one on — reported, never silently applied. */
|
|
36
|
+
refusal?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare const ADVISOR_OFF: AdvisorSettings;
|
|
39
|
+
/**
|
|
40
|
+
* Read the `advisor` block of a project settings file. Absent is off; malformed is off WITH a reason.
|
|
41
|
+
*
|
|
42
|
+
* The reason matters more than it looks: an operator who wrote `"enabeld": true` and got silence would conclude the
|
|
43
|
+
* feature does not work, and an operator who wrote it and got an advisor anyway would have a third party reading
|
|
44
|
+
* their session without having successfully asked for it. Both are worse than a sentence naming the field.
|
|
45
|
+
*/
|
|
46
|
+
export declare function advisorSettingsFrom(raw: unknown, env?: NodeJS.ProcessEnv): AdvisorSettings;
|
|
47
|
+
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/advisors/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAE,eAAe,IAAI,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;IACxB,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,WAAW,EAAE,eAAoE,CAAC;AAE/F;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,eAAe,CA4DvG"}
|