@hyperdrive.bot/paseo-protocol 0.3.35 → 0.3.37
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/agent-labels.d.ts +17 -0
- package/dist/agent-labels.js +42 -0
- package/dist/fleet/decisions.d.ts +178 -0
- package/dist/fleet/decisions.js +149 -0
- package/dist/fleet/params.d.ts +84 -0
- package/dist/fleet/params.js +142 -0
- package/dist/fleet/rpc-schemas.d.ts +792 -0
- package/dist/fleet/rpc-schemas.js +187 -0
- package/dist/fleet/types.d.ts +170 -0
- package/dist/fleet/types.js +80 -0
- package/dist/generated/validation/ws-outbound.aot.js +4061 -369
- package/dist/messages.d.ts +2332 -6
- package/dist/messages.js +31 -0
- package/dist/validation/ws-outbound-schema-metadata.d.ts +670 -1
- package/package.json +1 -1
package/dist/agent-labels.d.ts
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
export declare const PARENT_AGENT_ID_LABEL = "paseo.parent-agent-id";
|
|
2
2
|
export declare const WORKFLOW_ID_LABEL = "paseo.workflow-id";
|
|
3
3
|
export declare const WORKFLOW_TASK_ID_LABEL = "paseo.workflow-task-id";
|
|
4
|
+
export declare const LOOP_NAME_LABEL = "loop";
|
|
5
|
+
export declare const LOOP_RUN_LABEL = "run";
|
|
4
6
|
export interface AgentLabelSource {
|
|
5
7
|
labels?: Record<string, unknown> | null;
|
|
6
8
|
}
|
|
7
9
|
export declare function getParentAgentIdFromLabels(labels: Record<string, unknown> | null | undefined): string | null;
|
|
8
10
|
export declare function isDelegatedAgent(agent: AgentLabelSource): boolean;
|
|
11
|
+
export declare const LOOP_ADOPTED_AT_LABEL = "loop.adopted-at";
|
|
12
|
+
export declare function getLoopNameFromLabels(labels: Record<string, unknown> | null | undefined): string | null;
|
|
13
|
+
export declare function getLoopRunFromLabels(labels: Record<string, unknown> | null | undefined): string | null;
|
|
14
|
+
export declare function getLoopAdoptedAtFromLabels(labels: Record<string, unknown> | null | undefined): string | null;
|
|
15
|
+
/** Any agent a loop spawned, adopted or not. Use for provenance ("from loop X"). */
|
|
16
|
+
export declare function isLoopAgent(agent: AgentLabelSource): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* A loop execution still in loop custody, the set the general session surfaces hide.
|
|
19
|
+
* Adopted sessions return `false`: they are the user's now and belong back in the lists.
|
|
20
|
+
*
|
|
21
|
+
* Hiding is a LIST concern only. Attention surfaces (favicon, background activity, push)
|
|
22
|
+
* must keep counting these, otherwise a loop that needs a permission goes silent and the
|
|
23
|
+
* whole HITL story dies. See docs/loops.md.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isLoopExecutionAgent(agent: AgentLabelSource): boolean;
|
|
9
26
|
//# sourceMappingURL=agent-labels.d.ts.map
|
package/dist/agent-labels.js
CHANGED
|
@@ -11,6 +11,12 @@ export const WORKFLOW_ID_LABEL = "paseo.workflow-id";
|
|
|
11
11
|
// Children must already be scoped to a single workflow (via WORKFLOW_ID_LABEL) before
|
|
12
12
|
// being mapped — task ids are only unique within one workflow's graph.
|
|
13
13
|
export const WORKFLOW_TASK_ID_LABEL = "paseo.workflow-task-id";
|
|
14
|
+
// Loop fleet (super-repo automation loops surfaced in the UI as "Loops"). A loop
|
|
15
|
+
// execution agent carries loop=<loop-name> for per-loop attribution + the Executions
|
|
16
|
+
// view, and run=<timestamp> for the specific tick. Producer: the paseo engine in
|
|
17
|
+
// tooling/loops/lib/run-agent.sh (`paseo agent run --label loop=<name> --label run=<ts>`).
|
|
18
|
+
export const LOOP_NAME_LABEL = "loop";
|
|
19
|
+
export const LOOP_RUN_LABEL = "run";
|
|
14
20
|
export function getParentAgentIdFromLabels(labels) {
|
|
15
21
|
const parentAgentId = labels?.[PARENT_AGENT_ID_LABEL];
|
|
16
22
|
return typeof parentAgentId === "string" && parentAgentId.trim().length > 0
|
|
@@ -20,4 +26,40 @@ export function getParentAgentIdFromLabels(labels) {
|
|
|
20
26
|
export function isDelegatedAgent(agent) {
|
|
21
27
|
return getParentAgentIdFromLabels(agent.labels) !== null;
|
|
22
28
|
}
|
|
29
|
+
// Adoption (loop custody hand-off). A loop execution agent is hidden from the general
|
|
30
|
+
// session surfaces (Sessions, kanban, sidebar, command center) and appears only inside
|
|
31
|
+
// the Loops feature. Adoption is the escape hatch: once a human takes the session over,
|
|
32
|
+
// it leaves loop custody and behaves like any other session, keeping the `loop=<name>`
|
|
33
|
+
// label purely as lineage so the "from loop" provenance chip still renders.
|
|
34
|
+
//
|
|
35
|
+
// The rule, in one line: a loop session is invisible until you touch it. Reading or
|
|
36
|
+
// interrogating it does NOT count as touching; taking it over does.
|
|
37
|
+
export const LOOP_ADOPTED_AT_LABEL = "loop.adopted-at";
|
|
38
|
+
export function getLoopNameFromLabels(labels) {
|
|
39
|
+
const loopName = labels?.[LOOP_NAME_LABEL];
|
|
40
|
+
return typeof loopName === "string" && loopName.trim().length > 0 ? loopName.trim() : null;
|
|
41
|
+
}
|
|
42
|
+
export function getLoopRunFromLabels(labels) {
|
|
43
|
+
const run = labels?.[LOOP_RUN_LABEL];
|
|
44
|
+
return typeof run === "string" && run.trim().length > 0 ? run.trim() : null;
|
|
45
|
+
}
|
|
46
|
+
export function getLoopAdoptedAtFromLabels(labels) {
|
|
47
|
+
const adoptedAt = labels?.[LOOP_ADOPTED_AT_LABEL];
|
|
48
|
+
return typeof adoptedAt === "string" && adoptedAt.trim().length > 0 ? adoptedAt.trim() : null;
|
|
49
|
+
}
|
|
50
|
+
/** Any agent a loop spawned, adopted or not. Use for provenance ("from loop X"). */
|
|
51
|
+
export function isLoopAgent(agent) {
|
|
52
|
+
return getLoopNameFromLabels(agent.labels) !== null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A loop execution still in loop custody, the set the general session surfaces hide.
|
|
56
|
+
* Adopted sessions return `false`: they are the user's now and belong back in the lists.
|
|
57
|
+
*
|
|
58
|
+
* Hiding is a LIST concern only. Attention surfaces (favicon, background activity, push)
|
|
59
|
+
* must keep counting these, otherwise a loop that needs a permission goes silent and the
|
|
60
|
+
* whole HITL story dies. See docs/loops.md.
|
|
61
|
+
*/
|
|
62
|
+
export function isLoopExecutionAgent(agent) {
|
|
63
|
+
return isLoopAgent(agent) && getLoopAdoptedAtFromLabels(agent.labels) === null;
|
|
64
|
+
}
|
|
23
65
|
//# sourceMappingURL=agent-labels.js.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Loop decisions: the typed question a loop asks a human, and the record of how it
|
|
4
|
+
* was answered.
|
|
5
|
+
*
|
|
6
|
+
* A loop that needs a person does NOT emit a string and hope. It emits options with
|
|
7
|
+
* effects, so the UI can render buttons, a phone notification can carry the actual
|
|
8
|
+
* choices, and the resolution is attributable afterwards.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* What silence means. This is the field the UI must surface FIRST on every decision,
|
|
12
|
+
* because with a per-decision posture the same empty inbox can mean three different
|
|
13
|
+
* things: nothing is happening, work is frozen waiting for you, or work already
|
|
14
|
+
* happened while you were away.
|
|
15
|
+
*
|
|
16
|
+
* - `blocking` the item freezes until answered (the loop keeps ticking other items)
|
|
17
|
+
* - `deferring` the item is skipped this tick and re-offered on the next one
|
|
18
|
+
* - `advisory` the option runs now, you are told, and you can revoke it in the window
|
|
19
|
+
*/
|
|
20
|
+
export declare const LoopDecisionPostureSchema: z.ZodEnum<{
|
|
21
|
+
blocking: "blocking";
|
|
22
|
+
deferring: "deferring";
|
|
23
|
+
advisory: "advisory";
|
|
24
|
+
}>;
|
|
25
|
+
export type LoopDecisionPosture = z.infer<typeof LoopDecisionPostureSchema>;
|
|
26
|
+
/**
|
|
27
|
+
* How undoable an option's effect is. Posture is BOUNDED by this: you cannot repent of
|
|
28
|
+
* a sacrament, so an irreversible effect may never be `advisory` (see
|
|
29
|
+
* `assertDecisionIsSound`). Advisory on an irreversible act would mean the loop acts
|
|
30
|
+
* unsupervised with an undo button that cannot actually undo.
|
|
31
|
+
*/
|
|
32
|
+
export declare const LoopEffectReversibilitySchema: z.ZodEnum<{
|
|
33
|
+
reversible: "reversible";
|
|
34
|
+
compensable: "compensable";
|
|
35
|
+
irreversible: "irreversible";
|
|
36
|
+
}>;
|
|
37
|
+
export type LoopEffectReversibility = z.infer<typeof LoopEffectReversibilitySchema>;
|
|
38
|
+
export declare const LoopDecisionEffectSchema: z.ZodObject<{
|
|
39
|
+
kind: z.ZodEnum<{
|
|
40
|
+
act: "act";
|
|
41
|
+
rehearse: "rehearse";
|
|
42
|
+
defer: "defer";
|
|
43
|
+
suppress: "suppress";
|
|
44
|
+
}>;
|
|
45
|
+
reversibility: z.ZodEnum<{
|
|
46
|
+
reversible: "reversible";
|
|
47
|
+
compensable: "compensable";
|
|
48
|
+
irreversible: "irreversible";
|
|
49
|
+
}>;
|
|
50
|
+
compensatingAction: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
export type LoopDecisionEffect = z.infer<typeof LoopDecisionEffectSchema>;
|
|
53
|
+
export declare const LoopDecisionOptionSchema: z.ZodObject<{
|
|
54
|
+
id: z.ZodString;
|
|
55
|
+
label: z.ZodString;
|
|
56
|
+
effect: z.ZodObject<{
|
|
57
|
+
kind: z.ZodEnum<{
|
|
58
|
+
act: "act";
|
|
59
|
+
rehearse: "rehearse";
|
|
60
|
+
defer: "defer";
|
|
61
|
+
suppress: "suppress";
|
|
62
|
+
}>;
|
|
63
|
+
reversibility: z.ZodEnum<{
|
|
64
|
+
reversible: "reversible";
|
|
65
|
+
compensable: "compensable";
|
|
66
|
+
irreversible: "irreversible";
|
|
67
|
+
}>;
|
|
68
|
+
compensatingAction: z.ZodOptional<z.ZodString>;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
export type LoopDecisionOption = z.infer<typeof LoopDecisionOptionSchema>;
|
|
72
|
+
export declare const LoopDecisionKindSchema: z.ZodEnum<{
|
|
73
|
+
"gate-approval": "gate-approval";
|
|
74
|
+
"action-approval": "action-approval";
|
|
75
|
+
"outcome-review": "outcome-review";
|
|
76
|
+
}>;
|
|
77
|
+
export type LoopDecisionKind = z.infer<typeof LoopDecisionKindSchema>;
|
|
78
|
+
export declare const LoopDecisionResolverSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
79
|
+
kind: z.ZodLiteral<"human">;
|
|
80
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
82
|
+
kind: z.ZodLiteral<"expiry">;
|
|
83
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
84
|
+
kind: z.ZodLiteral<"policy">;
|
|
85
|
+
rule: z.ZodString;
|
|
86
|
+
}, z.core.$strip>], "kind">;
|
|
87
|
+
export type LoopDecisionResolver = z.infer<typeof LoopDecisionResolverSchema>;
|
|
88
|
+
export declare const LoopDecisionSchema: z.ZodObject<{
|
|
89
|
+
id: z.ZodString;
|
|
90
|
+
loopName: z.ZodString;
|
|
91
|
+
runId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
92
|
+
itemKey: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
93
|
+
kind: z.ZodEnum<{
|
|
94
|
+
"gate-approval": "gate-approval";
|
|
95
|
+
"action-approval": "action-approval";
|
|
96
|
+
"outcome-review": "outcome-review";
|
|
97
|
+
}>;
|
|
98
|
+
question: z.ZodString;
|
|
99
|
+
evidence: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
100
|
+
posture: z.ZodEnum<{
|
|
101
|
+
blocking: "blocking";
|
|
102
|
+
deferring: "deferring";
|
|
103
|
+
advisory: "advisory";
|
|
104
|
+
}>;
|
|
105
|
+
options: z.ZodArray<z.ZodObject<{
|
|
106
|
+
id: z.ZodString;
|
|
107
|
+
label: z.ZodString;
|
|
108
|
+
effect: z.ZodObject<{
|
|
109
|
+
kind: z.ZodEnum<{
|
|
110
|
+
act: "act";
|
|
111
|
+
rehearse: "rehearse";
|
|
112
|
+
defer: "defer";
|
|
113
|
+
suppress: "suppress";
|
|
114
|
+
}>;
|
|
115
|
+
reversibility: z.ZodEnum<{
|
|
116
|
+
reversible: "reversible";
|
|
117
|
+
compensable: "compensable";
|
|
118
|
+
irreversible: "irreversible";
|
|
119
|
+
}>;
|
|
120
|
+
compensatingAction: z.ZodOptional<z.ZodString>;
|
|
121
|
+
}, z.core.$strip>;
|
|
122
|
+
}, z.core.$strip>>;
|
|
123
|
+
onExpiry: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
124
|
+
expiresAt: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
125
|
+
createdAt: z.ZodString;
|
|
126
|
+
resolvedAt: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
127
|
+
resolvedOptionId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
128
|
+
resolvedBy: z.ZodDefault<z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
129
|
+
kind: z.ZodLiteral<"human">;
|
|
130
|
+
userId: z.ZodOptional<z.ZodString>;
|
|
131
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
132
|
+
kind: z.ZodLiteral<"expiry">;
|
|
133
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
134
|
+
kind: z.ZodLiteral<"policy">;
|
|
135
|
+
rule: z.ZodString;
|
|
136
|
+
}, z.core.$strip>], "kind">>>;
|
|
137
|
+
}, z.core.$strip>;
|
|
138
|
+
export type LoopDecision = z.infer<typeof LoopDecisionSchema>;
|
|
139
|
+
export interface CreateLoopDecisionInput {
|
|
140
|
+
loopName: string;
|
|
141
|
+
kind: LoopDecisionKind;
|
|
142
|
+
question: string;
|
|
143
|
+
posture: LoopDecisionPosture;
|
|
144
|
+
options: LoopDecisionOption[];
|
|
145
|
+
runId?: string | null;
|
|
146
|
+
itemKey?: string | null;
|
|
147
|
+
evidence?: string | null;
|
|
148
|
+
onExpiry?: string | null;
|
|
149
|
+
/** Relative lifetime; the service stamps `expiresAt` from it. */
|
|
150
|
+
expiresInMs?: number | null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Reject decisions that are unsound rather than letting them reach a human.
|
|
154
|
+
*
|
|
155
|
+
* Two rules, both load-bearing:
|
|
156
|
+
* 1. advisory + irreversible is refused outright. Not warned about: refused.
|
|
157
|
+
* 2. anything that can expire must say what expiring does, and that option must exist.
|
|
158
|
+
*/
|
|
159
|
+
export declare function assertDecisionIsSound(input: {
|
|
160
|
+
posture: LoopDecisionPosture;
|
|
161
|
+
options: readonly LoopDecisionOption[];
|
|
162
|
+
onExpiry?: string | null;
|
|
163
|
+
expiresAt?: string | null;
|
|
164
|
+
expiresInMs?: number | null;
|
|
165
|
+
}): void;
|
|
166
|
+
/** Decisions still awaiting an answer. */
|
|
167
|
+
export declare function isDecisionPending(decision: LoopDecision): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Sort key for the decision inbox: ascending time-to-consequence.
|
|
170
|
+
*
|
|
171
|
+
* Decisions that never expire sort LAST, which is deliberately counterintuitive. They
|
|
172
|
+
* feel the most urgent, yet they are the only ones where nothing happens if you look
|
|
173
|
+
* tomorrow. The ticking ones are where inaction is a decision you did not know you
|
|
174
|
+
* were making.
|
|
175
|
+
*/
|
|
176
|
+
export declare function decisionUrgency(decision: LoopDecision, now: number): number;
|
|
177
|
+
export declare function sortDecisionsByUrgency(decisions: readonly LoopDecision[], now: number): LoopDecision[];
|
|
178
|
+
//# sourceMappingURL=decisions.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Loop decisions: the typed question a loop asks a human, and the record of how it
|
|
4
|
+
* was answered.
|
|
5
|
+
*
|
|
6
|
+
* A loop that needs a person does NOT emit a string and hope. It emits options with
|
|
7
|
+
* effects, so the UI can render buttons, a phone notification can carry the actual
|
|
8
|
+
* choices, and the resolution is attributable afterwards.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* What silence means. This is the field the UI must surface FIRST on every decision,
|
|
12
|
+
* because with a per-decision posture the same empty inbox can mean three different
|
|
13
|
+
* things: nothing is happening, work is frozen waiting for you, or work already
|
|
14
|
+
* happened while you were away.
|
|
15
|
+
*
|
|
16
|
+
* - `blocking` the item freezes until answered (the loop keeps ticking other items)
|
|
17
|
+
* - `deferring` the item is skipped this tick and re-offered on the next one
|
|
18
|
+
* - `advisory` the option runs now, you are told, and you can revoke it in the window
|
|
19
|
+
*/
|
|
20
|
+
export const LoopDecisionPostureSchema = z.enum(["blocking", "deferring", "advisory"]);
|
|
21
|
+
/**
|
|
22
|
+
* How undoable an option's effect is. Posture is BOUNDED by this: you cannot repent of
|
|
23
|
+
* a sacrament, so an irreversible effect may never be `advisory` (see
|
|
24
|
+
* `assertDecisionIsSound`). Advisory on an irreversible act would mean the loop acts
|
|
25
|
+
* unsupervised with an undo button that cannot actually undo.
|
|
26
|
+
*/
|
|
27
|
+
export const LoopEffectReversibilitySchema = z.enum([
|
|
28
|
+
/** A clean inverse exists and the loop owns it (close the MR, delete the branch). */
|
|
29
|
+
"reversible",
|
|
30
|
+
/** No inverse, but a visible correction is possible (post a retraction). */
|
|
31
|
+
"compensable",
|
|
32
|
+
/** It left your control: message delivered, money moved, prod touched. */
|
|
33
|
+
"irreversible",
|
|
34
|
+
]);
|
|
35
|
+
export const LoopDecisionEffectSchema = z.object({
|
|
36
|
+
kind: z.enum(["act", "rehearse", "defer", "suppress"]),
|
|
37
|
+
reversibility: LoopEffectReversibilitySchema,
|
|
38
|
+
/**
|
|
39
|
+
* Required for a `compensable` effect offered under an `advisory` posture: the UI
|
|
40
|
+
* must name the correction ("post a retraction"), never a generic "undo".
|
|
41
|
+
*/
|
|
42
|
+
compensatingAction: z.string().trim().min(1).optional(),
|
|
43
|
+
});
|
|
44
|
+
export const LoopDecisionOptionSchema = z.object({
|
|
45
|
+
id: z.string().trim().min(1),
|
|
46
|
+
label: z.string().trim().min(1),
|
|
47
|
+
effect: LoopDecisionEffectSchema,
|
|
48
|
+
});
|
|
49
|
+
export const LoopDecisionKindSchema = z.enum([
|
|
50
|
+
/** Before any action: "the gate found 6, act on these?" */
|
|
51
|
+
"gate-approval",
|
|
52
|
+
/** Mid-run, for one item. */
|
|
53
|
+
"action-approval",
|
|
54
|
+
/** After the fact: "it says it opened MR !412, did it?" */
|
|
55
|
+
"outcome-review",
|
|
56
|
+
]);
|
|
57
|
+
export const LoopDecisionResolverSchema = z.discriminatedUnion("kind", [
|
|
58
|
+
z.object({ kind: z.literal("human"), userId: z.string().optional() }),
|
|
59
|
+
/** Resolved by the clock running out, applying `onExpiry`. Never silent. */
|
|
60
|
+
z.object({ kind: z.literal("expiry") }),
|
|
61
|
+
z.object({ kind: z.literal("policy"), rule: z.string() }),
|
|
62
|
+
]);
|
|
63
|
+
export const LoopDecisionSchema = z.object({
|
|
64
|
+
id: z.string(),
|
|
65
|
+
loopName: z.string().trim().min(1),
|
|
66
|
+
runId: z.string().nullable().default(null),
|
|
67
|
+
itemKey: z.string().nullable().default(null),
|
|
68
|
+
kind: LoopDecisionKindSchema,
|
|
69
|
+
question: z.string().trim().min(1),
|
|
70
|
+
/** What the gate saw, rendered next to the question so the answer is informed. */
|
|
71
|
+
evidence: z.string().nullable().default(null),
|
|
72
|
+
posture: LoopDecisionPostureSchema,
|
|
73
|
+
options: z.array(LoopDecisionOptionSchema).min(1),
|
|
74
|
+
/**
|
|
75
|
+
* Which option fires when the clock runs out. REQUIRED unless the decision is
|
|
76
|
+
* `blocking` with no expiry, i.e. the only shape where waiting forever is the
|
|
77
|
+
* declared behaviour. An author who will not say what happens when nobody answers
|
|
78
|
+
* has not finished thinking about the decision.
|
|
79
|
+
*/
|
|
80
|
+
onExpiry: z.string().nullable().default(null),
|
|
81
|
+
expiresAt: z.string().nullable().default(null),
|
|
82
|
+
createdAt: z.string(),
|
|
83
|
+
resolvedAt: z.string().nullable().default(null),
|
|
84
|
+
resolvedOptionId: z.string().nullable().default(null),
|
|
85
|
+
resolvedBy: LoopDecisionResolverSchema.nullable().default(null),
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Reject decisions that are unsound rather than letting them reach a human.
|
|
89
|
+
*
|
|
90
|
+
* Two rules, both load-bearing:
|
|
91
|
+
* 1. advisory + irreversible is refused outright. Not warned about: refused.
|
|
92
|
+
* 2. anything that can expire must say what expiring does, and that option must exist.
|
|
93
|
+
*/
|
|
94
|
+
export function assertDecisionIsSound(input) {
|
|
95
|
+
const ids = new Set();
|
|
96
|
+
for (const option of input.options) {
|
|
97
|
+
if (ids.has(option.id)) {
|
|
98
|
+
throw new Error(`Duplicate decision option id: ${option.id}`);
|
|
99
|
+
}
|
|
100
|
+
ids.add(option.id);
|
|
101
|
+
if (input.posture === "advisory" && option.effect.reversibility === "irreversible") {
|
|
102
|
+
throw new Error(`Option "${option.id}" is irreversible and cannot be offered under an advisory posture: ` +
|
|
103
|
+
"it would act unsupervised with an undo that cannot undo.");
|
|
104
|
+
}
|
|
105
|
+
if (input.posture === "advisory" &&
|
|
106
|
+
option.effect.reversibility === "compensable" &&
|
|
107
|
+
!option.effect.compensatingAction) {
|
|
108
|
+
throw new Error(`Option "${option.id}" is compensable under an advisory posture and must name its ` +
|
|
109
|
+
"compensating action, so the UI can offer it by name instead of a generic undo.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const canExpire = input.expiresAt != null || (input.expiresInMs != null && input.expiresInMs > 0);
|
|
113
|
+
const waitsForever = input.posture === "blocking" && !canExpire;
|
|
114
|
+
if (!waitsForever && !input.onExpiry) {
|
|
115
|
+
throw new Error("onExpiry is required: a decision that can time out must declare what timing out does.");
|
|
116
|
+
}
|
|
117
|
+
if (input.onExpiry && !ids.has(input.onExpiry)) {
|
|
118
|
+
throw new Error(`onExpiry "${input.onExpiry}" is not one of the decision's options.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Decisions still awaiting an answer. */
|
|
122
|
+
export function isDecisionPending(decision) {
|
|
123
|
+
return decision.resolvedAt === null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Sort key for the decision inbox: ascending time-to-consequence.
|
|
127
|
+
*
|
|
128
|
+
* Decisions that never expire sort LAST, which is deliberately counterintuitive. They
|
|
129
|
+
* feel the most urgent, yet they are the only ones where nothing happens if you look
|
|
130
|
+
* tomorrow. The ticking ones are where inaction is a decision you did not know you
|
|
131
|
+
* were making.
|
|
132
|
+
*/
|
|
133
|
+
export function decisionUrgency(decision, now) {
|
|
134
|
+
if (decision.expiresAt === null) {
|
|
135
|
+
return Number.POSITIVE_INFINITY;
|
|
136
|
+
}
|
|
137
|
+
return new Date(decision.expiresAt).getTime() - now;
|
|
138
|
+
}
|
|
139
|
+
export function sortDecisionsByUrgency(decisions, now) {
|
|
140
|
+
return [...decisions].sort((left, right) => {
|
|
141
|
+
const delta = decisionUrgency(left, now) - decisionUrgency(right, now);
|
|
142
|
+
if (Number.isNaN(delta) || delta === 0) {
|
|
143
|
+
// Stable tiebreak so the inbox does not reshuffle between renders.
|
|
144
|
+
return left.createdAt.localeCompare(right.createdAt);
|
|
145
|
+
}
|
|
146
|
+
return delta;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=decisions.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Loop parameters: a loop declares the knobs it takes, and the client renders the
|
|
4
|
+
* config form from that declaration instead of hard-coding one form per loop.
|
|
5
|
+
*
|
|
6
|
+
* This is deliberately NOT full JSON Schema. A loop's knobs are a flat list of scalar
|
|
7
|
+
* fields, and a small closed vocabulary is renderable, validatable, and reviewable;
|
|
8
|
+
* full JSON Schema would let a loop declare shapes the form could not draw, which
|
|
9
|
+
* turns "schema-driven" into "sometimes-driven".
|
|
10
|
+
*
|
|
11
|
+
* On disk, inside the loop's own directory:
|
|
12
|
+
* params.schema.json the declaration (committed with the loop)
|
|
13
|
+
* params.json the current values (written by the app)
|
|
14
|
+
*/
|
|
15
|
+
export declare const LoopParamTypeSchema: z.ZodEnum<{
|
|
16
|
+
string: "string";
|
|
17
|
+
number: "number";
|
|
18
|
+
boolean: "boolean";
|
|
19
|
+
enum: "enum";
|
|
20
|
+
"string-list": "string-list";
|
|
21
|
+
}>;
|
|
22
|
+
export type LoopParamType = z.infer<typeof LoopParamTypeSchema>;
|
|
23
|
+
export declare const LoopParamFieldSchema: z.ZodObject<{
|
|
24
|
+
key: z.ZodString;
|
|
25
|
+
label: z.ZodString;
|
|
26
|
+
type: z.ZodEnum<{
|
|
27
|
+
string: "string";
|
|
28
|
+
number: "number";
|
|
29
|
+
boolean: "boolean";
|
|
30
|
+
enum: "enum";
|
|
31
|
+
"string-list": "string-list";
|
|
32
|
+
}>;
|
|
33
|
+
description: z.ZodOptional<z.ZodString>;
|
|
34
|
+
required: z.ZodDefault<z.ZodBoolean>;
|
|
35
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
36
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
38
|
+
default: z.ZodOptional<z.ZodUnknown>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
export type LoopParamField = z.infer<typeof LoopParamFieldSchema>;
|
|
41
|
+
export declare const LoopParamsSchemaSchema: z.ZodObject<{
|
|
42
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
43
|
+
key: z.ZodString;
|
|
44
|
+
label: z.ZodString;
|
|
45
|
+
type: z.ZodEnum<{
|
|
46
|
+
string: "string";
|
|
47
|
+
number: "number";
|
|
48
|
+
boolean: "boolean";
|
|
49
|
+
enum: "enum";
|
|
50
|
+
"string-list": "string-list";
|
|
51
|
+
}>;
|
|
52
|
+
description: z.ZodOptional<z.ZodString>;
|
|
53
|
+
required: z.ZodDefault<z.ZodBoolean>;
|
|
54
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
55
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
56
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
default: z.ZodOptional<z.ZodUnknown>;
|
|
58
|
+
}, z.core.$strip>>;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
export type LoopParamsSchema = z.infer<typeof LoopParamsSchemaSchema>;
|
|
61
|
+
export declare const LoopParamValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodString>]>;
|
|
62
|
+
export type LoopParamValue = z.infer<typeof LoopParamValueSchema>;
|
|
63
|
+
export type LoopParamValues = Record<string, LoopParamValue>;
|
|
64
|
+
export interface LoopParamIssue {
|
|
65
|
+
key: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate values against a loop's declared fields.
|
|
70
|
+
*
|
|
71
|
+
* Returns every issue rather than throwing on the first, so the form can mark all the
|
|
72
|
+
* bad fields at once instead of making the user resubmit to find the next one. Unknown
|
|
73
|
+
* keys are reported too: silently dropping one would let a user "save" a value the loop
|
|
74
|
+
* never receives.
|
|
75
|
+
*/
|
|
76
|
+
export declare function validateLoopParams(schema: LoopParamsSchema, values: LoopParamValues): LoopParamIssue[];
|
|
77
|
+
/** Values a loop starts with: every field's declared default, where it has one. */
|
|
78
|
+
export declare function defaultLoopParams(schema: LoopParamsSchema): LoopParamValues;
|
|
79
|
+
/**
|
|
80
|
+
* Render values as env vars for the loop's runner. Lists join on commas and booleans
|
|
81
|
+
* become 1/0, which is what a bash runner can consume without a JSON parser.
|
|
82
|
+
*/
|
|
83
|
+
export declare function loopParamsToEnv(values: LoopParamValues): Record<string, string>;
|
|
84
|
+
//# sourceMappingURL=params.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Loop parameters: a loop declares the knobs it takes, and the client renders the
|
|
4
|
+
* config form from that declaration instead of hard-coding one form per loop.
|
|
5
|
+
*
|
|
6
|
+
* This is deliberately NOT full JSON Schema. A loop's knobs are a flat list of scalar
|
|
7
|
+
* fields, and a small closed vocabulary is renderable, validatable, and reviewable;
|
|
8
|
+
* full JSON Schema would let a loop declare shapes the form could not draw, which
|
|
9
|
+
* turns "schema-driven" into "sometimes-driven".
|
|
10
|
+
*
|
|
11
|
+
* On disk, inside the loop's own directory:
|
|
12
|
+
* params.schema.json the declaration (committed with the loop)
|
|
13
|
+
* params.json the current values (written by the app)
|
|
14
|
+
*/
|
|
15
|
+
export const LoopParamTypeSchema = z.enum(["string", "number", "boolean", "enum", "string-list"]);
|
|
16
|
+
export const LoopParamFieldSchema = z.object({
|
|
17
|
+
key: z
|
|
18
|
+
.string()
|
|
19
|
+
.trim()
|
|
20
|
+
.min(1)
|
|
21
|
+
// Values are handed to a shell runner as env vars, so the key has to be a safe
|
|
22
|
+
// identifier rather than arbitrary text.
|
|
23
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "param key must be a valid identifier"),
|
|
24
|
+
label: z.string().trim().min(1),
|
|
25
|
+
type: LoopParamTypeSchema,
|
|
26
|
+
description: z.string().optional(),
|
|
27
|
+
required: z.boolean().default(false),
|
|
28
|
+
/** Allowed values, required when `type` is `enum`. */
|
|
29
|
+
options: z.array(z.string()).optional(),
|
|
30
|
+
min: z.number().optional(),
|
|
31
|
+
max: z.number().optional(),
|
|
32
|
+
default: z.unknown().optional(),
|
|
33
|
+
});
|
|
34
|
+
export const LoopParamsSchemaSchema = z.object({
|
|
35
|
+
fields: z.array(LoopParamFieldSchema),
|
|
36
|
+
});
|
|
37
|
+
export const LoopParamValueSchema = z.union([
|
|
38
|
+
z.string(),
|
|
39
|
+
z.number(),
|
|
40
|
+
z.boolean(),
|
|
41
|
+
z.array(z.string()),
|
|
42
|
+
]);
|
|
43
|
+
/**
|
|
44
|
+
* Validate values against a loop's declared fields.
|
|
45
|
+
*
|
|
46
|
+
* Returns every issue rather than throwing on the first, so the form can mark all the
|
|
47
|
+
* bad fields at once instead of making the user resubmit to find the next one. Unknown
|
|
48
|
+
* keys are reported too: silently dropping one would let a user "save" a value the loop
|
|
49
|
+
* never receives.
|
|
50
|
+
*/
|
|
51
|
+
export function validateLoopParams(schema, values) {
|
|
52
|
+
const issues = [];
|
|
53
|
+
const declared = new Set(schema.fields.map((field) => field.key));
|
|
54
|
+
for (const key of Object.keys(values)) {
|
|
55
|
+
if (!declared.has(key)) {
|
|
56
|
+
issues.push({ key, message: `"${key}" is not a parameter this loop declares` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const field of schema.fields) {
|
|
60
|
+
const value = values[field.key];
|
|
61
|
+
if (value === undefined || value === null || value === "") {
|
|
62
|
+
if (field.required) {
|
|
63
|
+
issues.push({ key: field.key, message: `${field.label} is required` });
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
issues.push(...validateField(field, value));
|
|
68
|
+
}
|
|
69
|
+
return issues;
|
|
70
|
+
}
|
|
71
|
+
/** Type-specific checks for one field, split out to keep each rule readable. */
|
|
72
|
+
function validateField(field, value) {
|
|
73
|
+
switch (field.type) {
|
|
74
|
+
case "string":
|
|
75
|
+
return typeof value === "string"
|
|
76
|
+
? []
|
|
77
|
+
: [{ key: field.key, message: `${field.label} must be text` }];
|
|
78
|
+
case "number":
|
|
79
|
+
return validateNumberField(field, value);
|
|
80
|
+
case "boolean":
|
|
81
|
+
return typeof value === "boolean"
|
|
82
|
+
? []
|
|
83
|
+
: [{ key: field.key, message: `${field.label} must be true or false` }];
|
|
84
|
+
case "enum":
|
|
85
|
+
return typeof value === "string" && (field.options ?? []).includes(value)
|
|
86
|
+
? []
|
|
87
|
+
: [
|
|
88
|
+
{
|
|
89
|
+
key: field.key,
|
|
90
|
+
message: `${field.label} must be one of: ${(field.options ?? []).join(", ")}`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
case "string-list":
|
|
94
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string")
|
|
95
|
+
? []
|
|
96
|
+
: [{ key: field.key, message: `${field.label} must be a list of text values` }];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function validateNumberField(field, value) {
|
|
100
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
101
|
+
return [{ key: field.key, message: `${field.label} must be a number` }];
|
|
102
|
+
}
|
|
103
|
+
const issues = [];
|
|
104
|
+
if (field.min !== undefined && value < field.min) {
|
|
105
|
+
issues.push({ key: field.key, message: `${field.label} must be at least ${field.min}` });
|
|
106
|
+
}
|
|
107
|
+
if (field.max !== undefined && value > field.max) {
|
|
108
|
+
issues.push({ key: field.key, message: `${field.label} must be at most ${field.max}` });
|
|
109
|
+
}
|
|
110
|
+
return issues;
|
|
111
|
+
}
|
|
112
|
+
/** Values a loop starts with: every field's declared default, where it has one. */
|
|
113
|
+
export function defaultLoopParams(schema) {
|
|
114
|
+
const values = {};
|
|
115
|
+
for (const field of schema.fields) {
|
|
116
|
+
const parsed = LoopParamValueSchema.safeParse(field.default);
|
|
117
|
+
if (parsed.success) {
|
|
118
|
+
values[field.key] = parsed.data;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return values;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Render values as env vars for the loop's runner. Lists join on commas and booleans
|
|
125
|
+
* become 1/0, which is what a bash runner can consume without a JSON parser.
|
|
126
|
+
*/
|
|
127
|
+
export function loopParamsToEnv(values) {
|
|
128
|
+
const env = {};
|
|
129
|
+
for (const [key, value] of Object.entries(values)) {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
env[key] = value.join(",");
|
|
132
|
+
}
|
|
133
|
+
else if (typeof value === "boolean") {
|
|
134
|
+
env[key] = value ? "1" : "0";
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
env[key] = String(value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return env;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=params.js.map
|