skip to content
Speculative fan-out + composite scoring02 / 05

Group Chat Drama Triage

Four reads of the room, one API call, zero emotional labour.

flow graph · graphOf(group-chat-triage)
parallel · read-the-roomRead the roomjoin · read-the-roomcollectask · heataskheatask · passive-aggressionaskpassive-aggressionask · about-measkabout-meask · topicasktopicstep · make-a-planstepMake a planheatpassiveaboutMetopic
  • calls jev
  • your code
  • leaf / emit
  • fork / join
packages/examples/src/group-chat-drama.ts
1/**
2 * Group Chat Drama Triage
3 * Pattern: speculative fan-out + composite scoring in code.
4 *
5 * Four independent reads of the same chat run in `parallel`. Because they share
6 * a state, the client merges them into ONE request automatically (Jev reads the
7 * state once and answers every question in parallel). Then plain code weighs
8 * the answers into a severity and a plan. Jev judges; your code decides.
9 */
10import { ask, chain, choice, noul, parallel, score, step, type OutputOf } from "jevchain";
11import type { Example } from "./types";
13const readTheRoom = parallel("read-the-room", {
14 title: "Read the room",
15 branches: {
16 heat: ask("heat", {
17 questions: {
18 heat: score("How heated is this conversation?", ["friendly", "a little tense", "people are upset", "full meltdown"]),
19 },
20 }),
21 passive: ask("passive-aggression", {
22 questions: { passive: noul("Is anyone in `messages` being passive-aggressive?") },
23 }),
24 aboutMe: ask("about-me", {
25 questions: { aboutMe: noul("Is the tension directed at the person named in `me`?") },
26 }),
27 topic: ask("topic", {
28 questions: {
29 topic: choice("What is the tension mostly about?", {
30 plans: "Scheduling, cancelled plans, who is coming to what",
31 money: "Splitting bills, who owes whom, Venmo requests",
32 feelings: "Someone feels left out, ignored, or disrespected",
33 nothing: "There is no real tension; it's banter or memes",
34 }),
35 },
36 }),
37 },
38});
40const WEIGHTS = { heat: 0.5, passive: 0.2, aboutMe: 0.3 };
42const plan = step("make-a-plan", (r: OutputOf<typeof readTheRoom>) => {
43 // Composite score: weights live in code, where you can tune and test them.
44 const severity =
45 WEIGHTS.heat * (r.heat.heat.score / 3) + WEIGHTS.passive * r.passive.passive.noul + WEIGHTS.aboutMe * r.aboutMe.aboutMe.noul;
46 const topic = r.topic.topic.choice;
47 const advice =
48 severity < 0.25
49 ? "All good. Send a meme and carry on."
50 : severity < 0.5
51 ? topic === "money"
52 ? "Pay what you owe. Screenshot the receipt. Say nothing else."
53 : "Mute for one hour. Hydrate. Re-read before replying."
54 : severity < 0.75
55 ? topic === "feelings"
56 ? "Take it to DMs. Lead with 'hey, are we good?'"
57 : "Call them. Like, with your voice."
58 : "Leave the group. Change your name. Move to a new city.";
59 return { severity: Math.round(severity * 100) / 100, topic, advice };
60}, { title: "Make a plan" });
62export const groupChatDrama = chain("group-chat-triage", readTheRoom, plan);
64export const example: Example = {
65 slug: "group-chat-drama",
66 title: "Group Chat Drama Triage",
67 tagline: "Four reads of the room, one API call, zero emotional labour.",
68 pattern: "Speculative fan-out + composite scoring",
69 lesson:
70 "Fan out independent questions with parallel; asks against the same state are batched into a single request for free. Combine the answers with weights in code, where they're easy to test and tune.",
71 chain: groupChatDrama,
72 file: "group-chat-drama.ts",
73 inputs: [
74 {
75 label: "The brunch incident",
76 value: {
77 me: "Sam",
78 messages: [
79 { from: "Priya", text: "so are we still doing brunch sunday" },
80 { from: "Sam", text: "can't this week sorry!!" },
81 { from: "Jordan", text: "classic" },
82 { from: "Priya", text: "no worries. we'll just plan around Sam again 🙂" },
83 { from: "Jordan", text: "third time this month but who's counting" },
84 ],
85 },
86 },
87 {
88 label: "Venmo standoff",
89 value: {
90 me: "Alex",
91 messages: [
92 { from: "Chris", text: "hey just a reminder the airbnb was $840 split 4 ways" },
93 { from: "Chris", text: "3 of you have paid" },
94 { from: "Chris", text: "Alex." },
95 { from: "Mo", text: "lmaooo" },
96 ],
97 },
98 },
99 {
100 label: "Wholesome",
101 value: {
102 me: "Robin",
103 messages: [
104 { from: "Kai", text: "look at this dog" },
105 { from: "Robin", text: "I would die for him" },
106 { from: "Lee", text: "he's a 10/10 best boy" },
107 ],
108 },
109 },
110 ],
111};

api key

bring your own typesafe key, or ride the shared one (rate-limited, be nice).

shared key
checking…
your key
not set

your key stays in this browser (localStorage, jevchain.byok). it only travels to this site's /api/jev proxy in an x-typesafe-key header, which forwards it to typesafe and immediately forgets it. nothing is logged or stored server-side. requests on your own key get a much roomier rate limit.

keyboard shortcuts

fewer clicks, more chains. these work anywhere outside a text field.