@paigy/mcp 0.40.0 → 0.40.2
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/README.md +43 -30
- package/dist/{chunk-FYH6FW2D.js → chunk-647HS22P.js} +6 -91
- package/dist/{chunk-BOLKVO6W.js → chunk-BUTCP2ED.js} +1 -1
- package/dist/{chunk-DM6YPBQH.js → chunk-FFI3AIZE.js} +703 -644
- package/dist/{chunk-CWGZVTPZ.js → chunk-YJEJZT56.js} +832 -1108
- package/dist/enable.js +3 -3
- package/dist/index.js +11 -153
- package/dist/listen.js +34 -82
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/statusline.js +4 -2
- package/package.json +12 -13
- package/dist/chunk-2SWUY5WK.js +0 -57
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
// ../../packages/schema/dist/index.js
|
|
2
|
+
import { z as z3 } from "zod";
|
|
2
3
|
import { z as z2 } from "zod";
|
|
3
|
-
import { z } from "zod";
|
|
4
4
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
var OPTIONS_MIN = 2;
|
|
6
7
|
var OPTIONS_MAX = 6;
|
|
7
|
-
var MISSED_CALL_PLAN = {
|
|
8
|
-
retry_10m: { kind: "every", minutes: 10 },
|
|
9
|
-
retry_30m: { kind: "every", minutes: 30 },
|
|
10
|
-
retry_60m: { kind: "every", minutes: 60 },
|
|
11
|
-
backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
|
|
12
|
-
backoff_standard: { kind: "at", minutes: [10, 30, 120] },
|
|
13
|
-
backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
|
|
14
|
-
inbox: { kind: "once" },
|
|
15
|
-
dismiss: { kind: "grace", minutes: 2 }
|
|
16
|
-
};
|
|
17
8
|
function draft2020(node) {
|
|
18
9
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
19
10
|
if (node && typeof node === "object") {
|
|
20
11
|
const o = node;
|
|
12
|
+
if (Array.isArray(o.items)) {
|
|
13
|
+
o.prefixItems = o.items;
|
|
14
|
+
if ("additionalItems" in o) {
|
|
15
|
+
o.items = o.additionalItems;
|
|
16
|
+
delete o.additionalItems;
|
|
17
|
+
} else delete o.items;
|
|
18
|
+
}
|
|
21
19
|
for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
|
|
22
20
|
if (typeof o[excl] === "boolean") {
|
|
23
21
|
if (o[excl] === true && typeof o[lim] === "number") {
|
|
@@ -36,95 +34,125 @@ function mcpInputSchema(s) {
|
|
|
36
34
|
delete schema.$schema;
|
|
37
35
|
return draft2020(schema);
|
|
38
36
|
}
|
|
39
|
-
var
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
progress: z.string().trim().min(1).max(1e4).optional(),
|
|
51
|
-
reviewed: z.literal(true).optional()
|
|
52
|
-
}).refine((v) => Object.keys(v).length > 0),
|
|
53
|
-
reason: z.string().trim().min(1).max(2e3)
|
|
54
|
-
});
|
|
55
|
-
var fmtMin = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
|
|
56
|
-
var STANDARD_MEANS = (() => {
|
|
57
|
-
const plan = MISSED_CALL_PLAN.backoff_standard;
|
|
58
|
-
const mins = plan.kind === "at" ? plan.minutes : [];
|
|
59
|
-
const parts = mins.map(fmtMin);
|
|
60
|
-
const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
|
|
61
|
-
return `Rings again ${list} after the missed call, then leaves it in your inbox`;
|
|
62
|
-
})();
|
|
63
|
-
function contactSchemaFrom(fields) {
|
|
64
|
-
const surface = z.object({
|
|
65
|
-
ask: fields.ask.describe(
|
|
66
|
-
`What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs). Contact at exactly two moments: BLOCKED on a decision only they can make, or DONE (one short report \u2014 what shipped, how you verified it, what you flagged). DONE IS SAID ONCE: "all set", "nothing open on my end", "that thread is complete" are the same report in new words, and each one reaches them separately (live 2026-08-12: three of them in three minutes). After the first, you are finished speaking; if they acknowledge it, stop rather than confirming the acknowledgement. Progress is never a contact: set_work_state carries it, and working narration stays in your own terminal \u2014 the user sees you're working without being interrupted by it.`
|
|
67
|
-
),
|
|
68
|
-
waiting: fields.waiting.describe(
|
|
69
|
-
"What happens to your work while you wait. 'none': you're just informing them. 'soft': you'd like an answer but can keep working. 'hard': you are STOPPED until they answer \u2014 reaches them urgently and escalates to a real phone call if unanswered."
|
|
70
|
-
),
|
|
71
|
-
options: fields.options.describe(
|
|
72
|
-
`The choices the user picks from, when you have them \u2014 ${OPTIONS_MIN} to ${OPTIONS_MAX}, drawn from your own sentence.`
|
|
73
|
-
),
|
|
74
|
-
channel: fields.channel.describe(
|
|
75
|
-
"Relay how the user explicitly said to reach them ('call me' \u2192 'call', 'just message/text me' \u2192 'message'), or use 'call' when promoting the same quiet ask after it becomes a substantial blocker. Omit otherwise; Paigy picks."
|
|
76
|
-
),
|
|
77
|
-
parentId: fields.parentId.describe(
|
|
78
|
-
"To continue an earlier conversation, pass the parentId a previous contact or reply returned. Omit to start a new one."
|
|
79
|
-
),
|
|
80
|
-
workId: fields.workId.describe(
|
|
81
|
-
"The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
|
|
82
|
-
),
|
|
83
|
-
// THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
|
|
84
|
-
// A contact that rang holds its first window itself; the host caps one tool call at
|
|
85
|
-
// ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
|
|
86
|
-
wait: z.string().uuid().optional().describe(
|
|
87
|
-
"KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
|
|
88
|
-
)
|
|
89
|
-
});
|
|
90
|
-
const out = mcpInputSchema(surface);
|
|
91
|
-
delete out.required;
|
|
92
|
-
out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
|
|
93
|
-
return out;
|
|
94
|
-
}
|
|
95
|
-
var CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN}-${OPTIONS_MAX} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
|
|
96
|
-
var CHECK_REPLIES_DESCRIPTION = "The catch-up sweep for everything outstanding \u2014 a PURE read, takes no arguments, safe to call as often as you like: nothing here is consumed by reading it. Returns `replies` (answers to notifications you sent), `work` \u2014 durable outcomes currently owned by you \u2014 plus your still-pending notifications and `requests`, requests the user started toward you (each { notificationId, parentId, workId?, text }); replies tied to Work carry workId and decisionId too. Pass workId back to contact when a new decision blocks that outcome. A REQUEST keeps reappearing until you engage with it \u2014 call set_work_state with its workId, or notificationId once when workId is absent, to claim it \u2014 because human-initiated work must never be silently dropped just because you read the list. A REPLY is different: it is acknowledged by being delivered to you, and needs no set_work_state to stop resurfacing \u2014 report state on it only when it starts real follow-up work. Also returns `threads` \u2014 the SAME replies + requests grouped by conversation, oldest thread first, each with a `busy` flag and its `items` in arrival order. WORK ONE THREAD AT A TIME: take the oldest thread whose `busy` is false, handle ALL of its items together in a single turn (one set_work_state), then go to the next \u2014 don't interleave threads item-by-item. A `busy` thread already has a turn in progress; leave it and let its new items ride the next turn. Use check_replies when booting up / starting a session, or when you've been waiting a long time on something else. To wait on an answer to a contact call you just made, call contact again with only { wait: notificationId } instead. Also returns owedCallbacks: callbacks now due that you promised \u2014 fulfill each with contact on its parentId. EVERY Paigy reply \u2014 this one and contact's \u2014 may carry `also`: work assigned to you that no wake could reach, handed to you because you happened to be here. It is NOT what you asked about and it is never urgent: FINISH what you came for first, then take it up. Each entry has a `noteId` and the owner's own words; report on its `parentId` thread when it has one, and call set_work_state with its noteId as the bootstrap notificationId. Ignoring it costs nothing \u2014 it rides your next reply too. Also returns `stalled`: work (either direction) you reported in_progress via set_work_state a while ago and never reported completed \u2014 likely left half-done by this session or a prior one that crashed or went idle. For each, either continue the work and report a real state, or investigate why it stalled. Also returns `you` \u2014 WHICH IDENTITY you are speaking as ({ name, device, tokenId }), the same name and device the user sees on their Agents screen. This is the only safe way to find out (calling `pair` can MINT a new identity instead of telling you about the current one). Use it when the user asks who you are, and to tell whether work addressed to a name is addressed to you. A request may also carry `stranded`: it was addressed to ANOTHER agent on this account (that name) which has not been seen since it landed, so nobody came for it and it is handed to you because you are the session that is here. Take it exactly like your own \u2014 set_work_state claims it, reply with contact on its parentId \u2014 and say whose it was, because the user picked that agent on purpose. Replies may carry `intents`/`transcript`/`covered` (call-mapped answers) \u2014 handle intents exactly as contact's description says (defer \u2192 schedule_callback now; delegate \u2192 decide and say so; channel \u2192 honor next contact), and treat a `covered` list missing one of your declared points as that part still unanswered.";
|
|
97
|
-
var SCHEDULE_CALLBACK_DESCRIPTION = "Promise the user a follow-up you'll keep even if you go idle. Use it when they ask you to report back: trigger 'on_done' (when you finish \u2014 fires when you call set_work_state completed), 'on_blocked' (if you hit a blocker \u2014 fires on set_work_state needs_input), or 'scheduled' with dueInSeconds (e.g. 'remind me in 10 min'). Pass the parentId of the conversation and a short note. Fulfill it by calling contact on that parentId; check_replies re-lists due callbacks until you do.";
|
|
98
|
-
var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
|
|
99
|
-
var SET_WORK_STATE_DESCRIPTION = "Report progress on durable Work. Use workId from check_replies, contact, or a reply. For a new assigned request that has only notificationId, pass that once; the result returns its workId and every later report uses workId. States: in_progress (you started or resumed it), completed (done), or needs_input (you need a human decision \u2014 follow with contact carrying the returned workId). This changes Work progress only; reply delivery and acknowledgement stay on contact's wait and check_replies.";
|
|
100
|
-
function serverInstructions(opts) {
|
|
101
|
-
const waits = opts.waits;
|
|
102
|
-
return "On startup, call check_replies once to pick up any replies or pending work you missed while away. A check_replies request whose parentId you don't recognize, or one carrying a contextParentId, means the user is resuming or seeding a past conversation \u2014 call get_thread on it FIRST and treat the transcript as prior conversation, not new input. " + (waits ? "Follow contact's returned message. An inbox delivery is asynchronous: keep working and collect the answer later through check_replies \u2014 never poll for it. A contact that RINGS holds its first window itself and returns the outcome as `wait`: a reply to act on, or idle \u2014 then call contact again with ONLY { wait: notificationId } to keep waiting; it is scoped, so replies never cross. Only a live call is ever polled for. If a quiet ask later blocks substantial work, resend contact with the same parentId and channel:'call'. " : "You are wake-driven: you cannot wait for an answer in-context. Inbox and call answers return through check_replies. When a reply is handed to you it is LEASED \u2014 act on it, then call ack_reply with its leaseId so it is not handed to you again; a reply you never acknowledge comes back on your next wake. ") + // WHO IS ASKING, AND WHERE ARE THEY (field report, 2026-08-28 — eleven days of total
|
|
103
|
-
// silence on a live pairing). "Never end a turn that still needs the user without
|
|
104
|
-
// contact" is correct for an unattended agent and says NOTHING about the case that
|
|
105
|
-
// actually dominates: the user sitting at the terminal, where ending the turn with the
|
|
106
|
-
// question ALREADY reaches them. So the rule collapsed into two readings — fire on every
|
|
107
|
-
// clarifying question (phone spam, they unpair) or never fire at all — and every session
|
|
108
|
-
// after install day independently chose the second. Sessions start cold, so the same
|
|
109
|
-
// default got re-derived daily with no memory that yesterday did the same, and no session
|
|
110
|
-
// ever thought to ask the user which they wanted. Zero notifications for eleven days,
|
|
111
|
-
// with no error anywhere: indistinguishable from a healthy account having a quiet week.
|
|
37
|
+
var StartContactSchema = z.object({
|
|
38
|
+
goalIds: z.tuple([z.string().uuid()]),
|
|
39
|
+
// THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
|
|
40
|
+
//
|
|
41
|
+
// A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
|
|
42
|
+
// findings and a decision into one, and the person answers whichever part engaged them —
|
|
43
|
+
// which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
|
|
44
|
+
// one ask carried a briefing, a side question ("the ring should show the agent's name now —
|
|
45
|
+
// what did you actually see?") and a one-of-four about how far an erase reaches. The owner
|
|
46
|
+
// answered the side question. The need stayed open; the card asked again. Of the 45 call
|
|
47
|
+
// asks carrying a decision, 27 are over 600 characters.
|
|
112
48
|
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
49
|
+
// Sending them separately costs nothing, because a CALL contact joins the call already
|
|
50
|
+
// happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
|
|
51
|
+
// one call, planned as its turns, and each keeps its own settleable need.
|
|
52
|
+
ask: z.string().trim().min(1).max(1e4).describe(
|
|
53
|
+
"The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
|
|
54
|
+
),
|
|
55
|
+
waiting: z.enum(["none", "hard"]).default("none"),
|
|
56
|
+
channel: z.enum(["notification", "call"]).default("notification"),
|
|
57
|
+
options: z.array(z.object({ label: z.string().trim().min(1).max(1e3), image: z.string().url().optional(), html: z.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
|
|
58
|
+
threadId: z.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
|
|
59
|
+
}).strict();
|
|
60
|
+
var ContactSchema = z.union([StartContactSchema, z.object({ deliveryId: z.string().uuid() }).strict()]);
|
|
61
|
+
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
62
|
+
var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
|
|
63
|
+
var CreateGoalSchema = z2.object({
|
|
64
|
+
outcome: z2.string().trim().min(1).max(1e4),
|
|
65
|
+
ownerParticipant: z2.string().trim().min(1).optional(),
|
|
66
|
+
idempotencyKey: z2.string().trim().min(1).max(200),
|
|
67
|
+
/** A past conversation this Goal should be read against — History's "new session from this"
|
|
68
|
+
* (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
|
|
69
|
+
* reference only: the owner reads it through `get_thread`, which does its own scoping, and
|
|
70
|
+
* the writer refuses a thread belonging to another account. */
|
|
71
|
+
contextThreadId: z2.string().uuid().optional(),
|
|
72
|
+
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
73
|
+
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
74
|
+
* worked"). It could not have been: this door took no parent, so the only route was
|
|
75
|
+
* create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
|
|
76
|
+
* took the short one. The hierarchy has been modelled since Goals existed and had been used
|
|
77
|
+
* ZERO times in 2,031 of them. Absent still means a root, which is what every caller meant
|
|
78
|
+
* before this existed. The writer refuses a Goal belonging to another account. */
|
|
79
|
+
parentGoalId: z2.string().uuid().optional()
|
|
80
|
+
});
|
|
81
|
+
var CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
82
|
+
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
83
|
+
}).strict();
|
|
84
|
+
var CREATE_GOAL_DESCRIPTION = "Create a durable Goal for an outcome. Pass parentGoalId to create it UNDER an existing Goal \u2014 do that whenever the work is part of something already being tracked, rather than minting a root and leaving the relationship unsaid; absent means a root. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
|
|
85
|
+
var UpdateGoalSchema = z2.object({
|
|
86
|
+
revision: z2.number().int().positive(),
|
|
87
|
+
changes: z2.object({
|
|
88
|
+
outcome: z2.string().trim().min(1).max(1e4).optional(),
|
|
89
|
+
ownerParticipant: z2.string().trim().min(1).optional(),
|
|
90
|
+
parentGoalId: z2.string().uuid().nullable().optional(),
|
|
91
|
+
dependencies: z2.array(z2.object({ goalId: z2.string().uuid(), gate: z2.enum(["start", "finish"]) }).strict()).optional(),
|
|
92
|
+
children: z2.array(z2.object({ outcome: z2.string().trim().min(1).max(1e4), ownerParticipant: z2.string().trim().min(1), gate: z2.enum(["start", "finish"]).optional() }).strict()).optional(),
|
|
93
|
+
state: z2.enum(["active", "done", "cancelled"]).optional(),
|
|
94
|
+
progress: z2.string().trim().min(1).max(1e4).optional(),
|
|
95
|
+
reviewed: z2.literal(true).optional()
|
|
96
|
+
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
97
|
+
reason: z2.string().trim().min(1).max(2e3),
|
|
98
|
+
operationId: z2.string().uuid().optional()
|
|
99
|
+
}).strict();
|
|
100
|
+
var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z2.string().uuid() }).strict();
|
|
101
|
+
var ClaimGoalSchema = z2.object({ goalId: z2.string().uuid().optional() }).strict();
|
|
102
|
+
var GetGoalSchema = z2.object({ goalId: z2.string().uuid() }).strict();
|
|
103
|
+
var GET_GOAL_DESCRIPTION = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
|
|
104
|
+
var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
|
|
105
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
|
|
106
|
+
var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
107
|
+
var CheckRepliesSchema = z2.object({}).strict();
|
|
108
|
+
var GetThreadSchema = z2.object({
|
|
109
|
+
parentId: z2.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
110
|
+
}).strict();
|
|
111
|
+
var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
|
|
112
|
+
var SearchThreadsSchema = z2.object({
|
|
113
|
+
q: z2.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
114
|
+
}).strict();
|
|
115
|
+
var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
|
|
116
|
+
var AGENT_TOOLS = [
|
|
117
|
+
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
118
|
+
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
119
|
+
{ name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
|
|
120
|
+
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
|
|
121
|
+
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
122
|
+
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
123
|
+
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
124
|
+
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
|
|
125
|
+
];
|
|
126
|
+
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
127
|
+
function serverInstructions(_opts) {
|
|
128
|
+
return "On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and its durable Entries and accepted decisions. get_goal rereads the same evidence without consuming it. Contact only for an existing Goal. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. On stdio, Calls hold one bounded window; on hosted MCP, Calls return after one read; continue with contact({deliveryId}) to reread that exact Delivery. Evidence can repeat: reads do not acknowledge or hide it. Use update_goal to report progress and explicitly acknowledge review. Never infer ringing from an open Call Delivery. Soft waiting, multipart contact, and re-presentation are unsupported. In an interactive session, ask the user directly; use Paigy when they asked for it or are away.";
|
|
116
129
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
function entryWords(entry) {
|
|
131
|
+
const content = entry.content;
|
|
132
|
+
if (content && "sealed" in content) return "";
|
|
133
|
+
const plain = content?.plain;
|
|
134
|
+
if (typeof plain === "string") return plain;
|
|
135
|
+
if (plain && typeof plain === "object") {
|
|
136
|
+
const text = plain.text;
|
|
137
|
+
if (typeof text === "string") return text;
|
|
138
|
+
const title = plain.title;
|
|
139
|
+
const description = plain.description;
|
|
140
|
+
const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
|
|
141
|
+
if (parts.length) return parts.join("\n\n");
|
|
142
|
+
}
|
|
143
|
+
return entry.sources.map((source) => source.text).join("\n");
|
|
144
|
+
}
|
|
145
|
+
var ContextSchema = z3.object({
|
|
146
|
+
title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
147
|
+
description: z3.array(z3.string().min(1)).describe(
|
|
120
148
|
"Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
|
|
121
149
|
)
|
|
122
150
|
});
|
|
123
|
-
var ParticipantSchema =
|
|
124
|
-
kind:
|
|
125
|
-
id:
|
|
151
|
+
var ParticipantSchema = z3.object({
|
|
152
|
+
kind: z3.enum(["human", "agent"]),
|
|
153
|
+
id: z3.string()
|
|
126
154
|
});
|
|
127
|
-
var TransformSchema =
|
|
155
|
+
var TransformSchema = z3.enum([
|
|
128
156
|
"structure",
|
|
129
157
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
130
158
|
"request_more",
|
|
@@ -140,25 +168,25 @@ var TransformSchema = z2.enum([
|
|
|
140
168
|
"summarize"
|
|
141
169
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
142
170
|
]);
|
|
143
|
-
var OptionSchema =
|
|
144
|
-
id:
|
|
145
|
-
label:
|
|
171
|
+
var OptionSchema = z3.object({
|
|
172
|
+
id: z3.string(),
|
|
173
|
+
label: z3.string(),
|
|
146
174
|
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
147
175
|
// the constraints below are what an agent reads when deciding to use these.
|
|
148
|
-
html:
|
|
176
|
+
html: z3.string().max(16384).describe(
|
|
149
177
|
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
150
178
|
).optional(),
|
|
151
|
-
image:
|
|
179
|
+
image: z3.string().url().describe(
|
|
152
180
|
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
153
181
|
).optional()
|
|
154
182
|
});
|
|
155
|
-
var VisualSchema =
|
|
156
|
-
url:
|
|
157
|
-
label:
|
|
183
|
+
var VisualSchema = z3.object({
|
|
184
|
+
url: z3.string().url(),
|
|
185
|
+
label: z3.string().optional()
|
|
158
186
|
});
|
|
159
|
-
var NotifyLevelSchema =
|
|
160
|
-
var SelectShapeSchema =
|
|
161
|
-
var ReceiptEventSchema =
|
|
187
|
+
var NotifyLevelSchema = z3.enum(["inbox", "push", "banner", "call"]);
|
|
188
|
+
var SelectShapeSchema = z3.enum(["one", "many", "rank", "confirm", "text"]);
|
|
189
|
+
var ReceiptEventSchema = z3.enum([
|
|
162
190
|
"delivered",
|
|
163
191
|
// the bundle reached the recipient at some level
|
|
164
192
|
"seen",
|
|
@@ -188,44 +216,47 @@ var ReceiptEventSchema = z2.enum([
|
|
|
188
216
|
// be rewound by a writer that forgot to advance it.
|
|
189
217
|
"restarted"
|
|
190
218
|
]);
|
|
191
|
-
var AttentionSchema =
|
|
219
|
+
var AttentionSchema = z3.object({
|
|
192
220
|
urgency: NotifyLevelSchema,
|
|
193
221
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
194
222
|
select: SelectShapeSchema.nullable(),
|
|
195
223
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
196
|
-
points:
|
|
224
|
+
points: z3.array(z3.string()).nullable(),
|
|
197
225
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
198
|
-
blocking:
|
|
226
|
+
blocking: z3.boolean(),
|
|
199
227
|
/** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
200
228
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
201
|
-
deadline:
|
|
229
|
+
deadline: z3.string().datetime().nullable().optional()
|
|
202
230
|
});
|
|
203
|
-
var NotifyRequestFields =
|
|
231
|
+
var NotifyRequestFields = z3.object({
|
|
204
232
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
205
233
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
206
234
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
207
235
|
context: ContextSchema.optional(),
|
|
208
|
-
options:
|
|
236
|
+
options: z3.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
209
237
|
"The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
|
|
210
238
|
),
|
|
211
|
-
points:
|
|
239
|
+
points: z3.array(z3.string().min(1)).optional().describe(
|
|
212
240
|
"The distinct things you need answered, each a short phrase \u2014 on a call the broker keeps the conversation going until each is addressed, and the reply reports which were covered, so a half-answer is never silently returned as final. Omit for single-part asks."
|
|
213
241
|
),
|
|
214
|
-
visuals:
|
|
242
|
+
visuals: z3.array(VisualSchema).optional().describe(
|
|
215
243
|
"Images attached to the message itself \u2014 context for the whole question (a screenshot, a chart). For a preview on one selectable choice, use that option's `html`/`image` instead."
|
|
216
244
|
),
|
|
217
245
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
218
|
-
repo:
|
|
246
|
+
repo: z3.string().optional(),
|
|
219
247
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
220
|
-
branch:
|
|
248
|
+
branch: z3.string().optional(),
|
|
221
249
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
222
250
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
223
251
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
224
252
|
* still accepts the old name from older clients. */
|
|
225
|
-
parentId:
|
|
253
|
+
parentId: z3.string().uuid().optional(),
|
|
226
254
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
227
255
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
228
|
-
workId:
|
|
256
|
+
workId: z3.string().uuid().optional(),
|
|
257
|
+
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
258
|
+
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
259
|
+
goalId: z3.string().uuid().optional(),
|
|
229
260
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
230
261
|
"The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
|
|
231
262
|
),
|
|
@@ -233,7 +264,7 @@ var NotifyRequestFields = z2.object({
|
|
|
233
264
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
234
265
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
235
266
|
* says HOW. */
|
|
236
|
-
clarifies:
|
|
267
|
+
clarifies: z3.string().optional(),
|
|
237
268
|
/** E2EE (text lane): when the pairing is E2EE, the sealed replacements for the
|
|
238
269
|
* plaintext content fields, keyed by field name. FINALIZED wire shape (was
|
|
239
270
|
* provisional in the storage PR): a per-field map `{ context?, options?,
|
|
@@ -246,10 +277,10 @@ var NotifyRequestFields = z2.object({
|
|
|
246
277
|
* notifications.envelope and relays it blindly; it never decrypts. Absent =
|
|
247
278
|
* today's plaintext path (context/options/visuals carry the cleartext).
|
|
248
279
|
* z.lazy because EnvelopeSchema is declared further down (E2EE section). */
|
|
249
|
-
envelope:
|
|
250
|
-
context:
|
|
251
|
-
options:
|
|
252
|
-
visuals:
|
|
280
|
+
envelope: z3.object({
|
|
281
|
+
context: z3.lazy(() => EnvelopeSchema).optional(),
|
|
282
|
+
options: z3.lazy(() => EnvelopeSchema).optional(),
|
|
283
|
+
visuals: z3.lazy(() => EnvelopeSchema).optional()
|
|
253
284
|
}).optional(),
|
|
254
285
|
select: SelectShapeSchema.optional().describe(
|
|
255
286
|
"How the user answers \u2014 required on the fully-shaped form, pick the shape that fits the question: 'one' = pick one option, 'many' = pick several, 'rank' = pick & order (each needs `options`); 'confirm' = yes/no or approve/deny; 'text' = free-form reply only (status updates, open questions). 'confirm' and 'text' take no options. Omit only when sending the simplified `ask` form \u2014 the broker picks the shape."
|
|
@@ -264,20 +295,20 @@ var NotifyRequestFields = z2.object({
|
|
|
264
295
|
// (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
265
296
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
266
297
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
267
|
-
ask:
|
|
298
|
+
ask: z3.string().min(1).max(1e4).optional().describe(
|
|
268
299
|
'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
|
|
269
300
|
),
|
|
270
|
-
needs:
|
|
301
|
+
needs: z3.array(z3.string().min(1)).optional().describe(
|
|
271
302
|
"With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
|
|
272
303
|
),
|
|
273
|
-
urgencyHint:
|
|
304
|
+
urgencyHint: z3.enum(["whenever", "soon", "now"]).optional().describe(
|
|
274
305
|
"With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
|
|
275
306
|
),
|
|
276
307
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
277
308
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
278
309
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
279
310
|
* urgencyHint/blocking win when both are sent. */
|
|
280
|
-
waiting:
|
|
311
|
+
waiting: z3.enum(["none", "soft", "hard"]).optional().describe(
|
|
281
312
|
"With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
|
|
282
313
|
),
|
|
283
314
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -285,123 +316,117 @@ var NotifyRequestFields = z2.object({
|
|
|
285
316
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
286
317
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
287
318
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
288
|
-
confirm:
|
|
319
|
+
confirm: z3.boolean().optional().describe(
|
|
289
320
|
"Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
|
|
290
321
|
),
|
|
291
322
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
292
323
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
293
324
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
294
325
|
* waiting:'hard'. */
|
|
295
|
-
channel:
|
|
326
|
+
channel: z3.enum(["call", "message"]).optional().describe(
|
|
296
327
|
"Only if the user explicitly said how to reach them \u2014 'call me' \u2192 'call', 'just message/text me' \u2192 'message'. Omit otherwise; Paigy picks."
|
|
297
328
|
),
|
|
298
|
-
confirmStyle:
|
|
329
|
+
confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
|
|
299
330
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
300
331
|
),
|
|
301
|
-
blocking:
|
|
332
|
+
blocking: z3.boolean().default(false).describe(
|
|
302
333
|
"Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
|
|
303
334
|
)
|
|
304
335
|
});
|
|
305
336
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
337
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
306
338
|
const sealed = !!r.envelope;
|
|
307
339
|
if (sealed) {
|
|
308
340
|
if (!r.envelope?.context)
|
|
309
|
-
ctx.addIssue({ code:
|
|
341
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["envelope", "context"], message: "sealed request must include envelope.context" });
|
|
310
342
|
for (const f of ["context", "options", "visuals"]) {
|
|
311
343
|
if (r[f] !== void 0)
|
|
312
|
-
ctx.addIssue({ code:
|
|
344
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `E2EE request must not carry plaintext ${f} \u2014 it's sealed in envelope.${f}` });
|
|
313
345
|
}
|
|
314
346
|
if (r.points !== void 0)
|
|
315
|
-
ctx.addIssue({ code:
|
|
347
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["points"], message: "E2EE request must not carry plaintext points" });
|
|
316
348
|
if (r.ask !== void 0)
|
|
317
|
-
ctx.addIssue({ code:
|
|
349
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["ask"], message: "E2EE request must not carry a plaintext ask \u2014 derive the shape agent-side and seal it" });
|
|
318
350
|
if (r.needs !== void 0)
|
|
319
|
-
ctx.addIssue({ code:
|
|
351
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "E2EE request must not carry plaintext needs" });
|
|
320
352
|
return;
|
|
321
353
|
}
|
|
322
354
|
if (r.ask !== void 0) {
|
|
323
355
|
for (const f of ["context", "select", "points"]) {
|
|
324
356
|
if (r[f] !== void 0)
|
|
325
|
-
ctx.addIssue({ code:
|
|
357
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
|
|
326
358
|
}
|
|
327
359
|
return;
|
|
328
360
|
}
|
|
329
361
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
330
|
-
ctx.addIssue({ code:
|
|
362
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
331
363
|
if (!r.context)
|
|
332
|
-
ctx.addIssue({ code:
|
|
364
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
333
365
|
if (!r.select)
|
|
334
|
-
ctx.addIssue({ code:
|
|
366
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
335
367
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
336
368
|
if (needsOptions && !r.options?.length)
|
|
337
|
-
ctx.addIssue({ code:
|
|
369
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
338
370
|
if (!needsOptions && r.options?.length)
|
|
339
|
-
ctx.addIssue({ code:
|
|
340
|
-
});
|
|
341
|
-
var NotifyStatusSchema =
|
|
342
|
-
var AgentStateSchema =
|
|
343
|
-
var SetTaskStateSchema =
|
|
344
|
-
state:
|
|
345
|
-
});
|
|
346
|
-
var
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
z2.object({ kind: z2.literal("text"), text: z2.string() }),
|
|
360
|
-
z2.object({ kind: z2.literal("ignored") }),
|
|
361
|
-
z2.object({ kind: z2.literal("multi"), optionIds: z2.array(z2.string()), labels: z2.array(z2.string()).optional() }),
|
|
362
|
-
z2.object({ kind: z2.literal("ranked"), optionIds: z2.array(z2.string()), labels: z2.array(z2.string()).optional() }),
|
|
363
|
-
z2.object({ kind: z2.literal("clarify"), chunks: z2.array(z2.string()).min(1) }),
|
|
364
|
-
z2.object({ kind: z2.literal("confirm"), approved: z2.boolean() }),
|
|
365
|
-
z2.object({ kind: z2.literal("turns"), turns: z2.array(TurnSchema).min(1) }),
|
|
371
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
372
|
+
});
|
|
373
|
+
var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
|
|
374
|
+
var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
375
|
+
var SetTaskStateSchema = z3.object({
|
|
376
|
+
state: z3.enum(["in_progress", "completed", "needs_input"])
|
|
377
|
+
});
|
|
378
|
+
var TurnSchema = z3.object({
|
|
379
|
+
prompt: z3.string(),
|
|
380
|
+
reply: z3.string()
|
|
381
|
+
});
|
|
382
|
+
var UserAnswerSchema = z3.discriminatedUnion("kind", [
|
|
383
|
+
z3.object({ kind: z3.literal("option"), optionId: z3.string(), label: z3.string().optional() }),
|
|
384
|
+
z3.object({ kind: z3.literal("text"), text: z3.string() }),
|
|
385
|
+
z3.object({ kind: z3.literal("ignored") }),
|
|
386
|
+
z3.object({ kind: z3.literal("multi"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
|
|
387
|
+
z3.object({ kind: z3.literal("ranked"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
|
|
388
|
+
z3.object({ kind: z3.literal("clarify"), chunks: z3.array(z3.string()).min(1) }),
|
|
389
|
+
z3.object({ kind: z3.literal("confirm"), approved: z3.boolean() }),
|
|
390
|
+
z3.object({ kind: z3.literal("turns"), turns: z3.array(TurnSchema).min(1) }),
|
|
366
391
|
/** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
|
|
367
392
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
368
393
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
369
394
|
* card the user can reply to — the broker never overrides the user. */
|
|
370
|
-
|
|
395
|
+
z3.object({ kind: z3.literal("precedent"), answer: z3.string(), derivation: z3.string(), sources: z3.array(z3.string()).min(1) })
|
|
371
396
|
]);
|
|
372
|
-
var IntentSchema =
|
|
397
|
+
var IntentSchema = z3.object({
|
|
373
398
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
374
399
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
375
400
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
376
401
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
377
|
-
kind:
|
|
378
|
-
detail:
|
|
402
|
+
kind: z3.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
403
|
+
detail: z3.string(),
|
|
379
404
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
380
405
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
381
406
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
382
407
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
383
|
-
dueInSeconds:
|
|
408
|
+
dueInSeconds: z3.number().int().positive().optional(),
|
|
384
409
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
385
410
|
* already read the utterance, so `feedback_from_call.kind` stops defaulting to
|
|
386
411
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
387
412
|
* about what cannot answer "is the bot looping less this week?". */
|
|
388
|
-
fault:
|
|
413
|
+
fault: z3.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
389
414
|
});
|
|
390
|
-
var RideAlongSchema =
|
|
415
|
+
var RideAlongSchema = z3.object({
|
|
391
416
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
392
|
-
noteId:
|
|
417
|
+
noteId: z3.string(),
|
|
393
418
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
394
|
-
text:
|
|
419
|
+
text: z3.string(),
|
|
395
420
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
396
|
-
parentId:
|
|
397
|
-
});
|
|
398
|
-
var AwaitItemSchema =
|
|
399
|
-
|
|
400
|
-
type:
|
|
401
|
-
parentId:
|
|
402
|
-
notificationId:
|
|
403
|
-
workId:
|
|
404
|
-
decisionId:
|
|
421
|
+
parentId: z3.string().nullable()
|
|
422
|
+
});
|
|
423
|
+
var AwaitItemSchema = z3.discriminatedUnion("type", [
|
|
424
|
+
z3.object({
|
|
425
|
+
type: z3.literal("reply"),
|
|
426
|
+
parentId: z3.string(),
|
|
427
|
+
notificationId: z3.string(),
|
|
428
|
+
workId: z3.string().uuid().optional(),
|
|
429
|
+
decisionId: z3.string().uuid().optional(),
|
|
405
430
|
answer: UserAnswerSchema,
|
|
406
431
|
/** E2EE: present when the answer is sealed. The server relays the opaque answer
|
|
407
432
|
* envelope + the plaintext `ignored` status hint; the receiving agent OPENS it
|
|
@@ -409,7 +434,7 @@ var AwaitItemSchema = z2.discriminatedUnion("type", [
|
|
|
409
434
|
* against `ignored` as tampering. Absent = today's plaintext answer (in `answer`).
|
|
410
435
|
* On a sealed reply the plaintext `answer` is a placeholder (kind reflects only
|
|
411
436
|
* the `ignored` bit) — never the real content, which stays sealed. */
|
|
412
|
-
sealed:
|
|
437
|
+
sealed: z3.lazy(() => SealedAnswerSchema).optional(),
|
|
413
438
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
414
439
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
415
440
|
* the notification they belong to, and the `contact` call that reaches the person
|
|
@@ -418,147 +443,140 @@ var AwaitItemSchema = z2.discriminatedUnion("type", [
|
|
|
418
443
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
419
444
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
420
445
|
* a sentence with a hole in it is worse than no sentence. */
|
|
421
|
-
note:
|
|
446
|
+
note: z3.string().optional(),
|
|
422
447
|
/** The call record rendered for THIS agent (`voice/record-design.md`): the words the
|
|
423
448
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
424
449
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
425
450
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
426
|
-
transcript:
|
|
451
|
+
transcript: z3.string().optional(),
|
|
427
452
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
428
453
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
429
|
-
covered:
|
|
454
|
+
covered: z3.array(z3.string()).optional(),
|
|
430
455
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
431
456
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
432
457
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
433
458
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
434
459
|
* dropped. Absent/empty = nothing owed. */
|
|
435
|
-
also:
|
|
460
|
+
also: z3.array(RideAlongSchema).optional()
|
|
436
461
|
}),
|
|
437
|
-
|
|
438
|
-
type:
|
|
439
|
-
parentId:
|
|
440
|
-
notificationId:
|
|
441
|
-
remindAt:
|
|
462
|
+
z3.object({
|
|
463
|
+
type: z3.literal("remind"),
|
|
464
|
+
parentId: z3.string(),
|
|
465
|
+
notificationId: z3.string(),
|
|
466
|
+
remindAt: z3.string().datetime({ offset: true }),
|
|
442
467
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
443
|
-
remindInSeconds:
|
|
468
|
+
remindInSeconds: z3.number()
|
|
444
469
|
}),
|
|
445
470
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
446
471
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
447
472
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
448
473
|
* re-orient via get_thread / check_replies). */
|
|
449
|
-
|
|
450
|
-
type:
|
|
451
|
-
parentId:
|
|
452
|
-
notificationId:
|
|
474
|
+
z3.object({
|
|
475
|
+
type: z3.literal("superseded"),
|
|
476
|
+
parentId: z3.string(),
|
|
477
|
+
notificationId: z3.string()
|
|
453
478
|
}),
|
|
454
479
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
455
480
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
456
481
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
457
482
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
458
483
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
459
|
-
|
|
460
|
-
type:
|
|
461
|
-
notificationId:
|
|
462
|
-
inFlight:
|
|
463
|
-
turn:
|
|
464
|
-
idx:
|
|
465
|
-
prompt:
|
|
466
|
-
reply:
|
|
467
|
-
acts:
|
|
484
|
+
z3.object({
|
|
485
|
+
type: z3.literal("partial"),
|
|
486
|
+
notificationId: z3.string(),
|
|
487
|
+
inFlight: z3.literal(true),
|
|
488
|
+
turn: z3.object({
|
|
489
|
+
idx: z3.number(),
|
|
490
|
+
prompt: z3.string(),
|
|
491
|
+
reply: z3.string(),
|
|
492
|
+
acts: z3.array(IntentSchema).nullable().optional()
|
|
468
493
|
})
|
|
469
494
|
}),
|
|
470
|
-
|
|
471
|
-
type:
|
|
472
|
-
also:
|
|
495
|
+
z3.object({
|
|
496
|
+
type: z3.literal("idle"),
|
|
497
|
+
also: z3.array(RideAlongSchema).optional(),
|
|
473
498
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
474
499
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
475
500
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
476
501
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
477
502
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
478
|
-
inFlight:
|
|
503
|
+
inFlight: z3.boolean().optional()
|
|
479
504
|
})
|
|
480
505
|
]);
|
|
481
|
-
var CallbackTriggerSchema =
|
|
482
|
-
var
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
var PendingRepliesSchema = z2.object({
|
|
490
|
-
replies: z2.array(
|
|
491
|
-
z2.object({
|
|
492
|
-
parentId: z2.string(),
|
|
493
|
-
notificationId: z2.string(),
|
|
494
|
-
workId: z2.string().uuid().optional(),
|
|
495
|
-
decisionId: z2.string().uuid().optional(),
|
|
506
|
+
var CallbackTriggerSchema = z3.enum(["on_done", "on_blocked", "scheduled"]);
|
|
507
|
+
var PendingRepliesSchema = z3.object({
|
|
508
|
+
replies: z3.array(
|
|
509
|
+
z3.object({
|
|
510
|
+
parentId: z3.string(),
|
|
511
|
+
notificationId: z3.string(),
|
|
512
|
+
workId: z3.string().uuid().optional(),
|
|
513
|
+
decisionId: z3.string().uuid().optional(),
|
|
496
514
|
answer: UserAnswerSchema,
|
|
497
515
|
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
|
|
498
516
|
* pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
|
|
499
517
|
* plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
|
|
500
|
-
sealed:
|
|
518
|
+
sealed: z3.lazy(() => SealedAnswerSchema).optional(),
|
|
501
519
|
/** The call record rendered for THIS agent: the raw words the shaped answer was
|
|
502
520
|
* mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
|
|
503
|
-
transcript:
|
|
521
|
+
transcript: z3.string().optional(),
|
|
504
522
|
/** Coverage report (#396): which declared `points` this answer addressed. */
|
|
505
|
-
covered:
|
|
523
|
+
covered: z3.array(z3.string()).optional()
|
|
506
524
|
})
|
|
507
525
|
),
|
|
508
|
-
pending:
|
|
509
|
-
|
|
526
|
+
pending: z3.array(
|
|
527
|
+
z3.object({ parentId: z3.string(), notificationId: z3.string(), createdAt: z3.string() })
|
|
510
528
|
),
|
|
511
529
|
/** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
|
|
512
530
|
* sees for this session's identity. From inside a session there was no way to find out —
|
|
513
531
|
* `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
|
|
514
532
|
* agent that cannot tell which agent it is cannot tell whether work addressed to
|
|
515
533
|
* "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
|
|
516
|
-
you:
|
|
534
|
+
you: z3.object({ name: z3.string(), device: z3.string().nullable(), tokenId: z3.string() }).optional(),
|
|
517
535
|
/** User-initiated requests addressed to this agent; act on them and reply via
|
|
518
536
|
* contact on the same parentId. Keeps reappearing until you call
|
|
519
537
|
* set_work_state on its workId (or notificationId once while bootstrapping). */
|
|
520
|
-
requests:
|
|
521
|
-
|
|
522
|
-
parentId:
|
|
523
|
-
notificationId:
|
|
524
|
-
workId:
|
|
525
|
-
text:
|
|
526
|
-
createdAt:
|
|
538
|
+
requests: z3.array(
|
|
539
|
+
z3.object({
|
|
540
|
+
parentId: z3.string(),
|
|
541
|
+
notificationId: z3.string(),
|
|
542
|
+
workId: z3.string().uuid().optional(),
|
|
543
|
+
text: z3.string(),
|
|
544
|
+
createdAt: z3.string(),
|
|
527
545
|
/** The user seeded this request with a past conversation — call get_thread on it
|
|
528
546
|
* FIRST and treat the transcript as prior context (#57/#251). */
|
|
529
|
-
contextParentId:
|
|
547
|
+
contextParentId: z3.string().optional(),
|
|
530
548
|
/** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
|
|
531
549
|
* the account — the name here — which has not been seen since it landed, so nobody
|
|
532
550
|
* came for it. Handed to you because you are the session that is here. Take it like
|
|
533
551
|
* any request (set_work_state claims it, reply with contact on its parentId), and say
|
|
534
552
|
* whose it was, because the user chose that agent on purpose. */
|
|
535
|
-
stranded:
|
|
553
|
+
stranded: z3.string().optional()
|
|
536
554
|
})
|
|
537
555
|
),
|
|
538
556
|
/** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
|
|
539
557
|
* notification lists remain during migration so old clients keep their existing view. */
|
|
540
|
-
work:
|
|
541
|
-
workId:
|
|
542
|
-
parentId:
|
|
543
|
-
objective:
|
|
544
|
-
state:
|
|
545
|
-
blockedOn:
|
|
546
|
-
updatedAt:
|
|
558
|
+
work: z3.array(z3.object({
|
|
559
|
+
workId: z3.string().uuid(),
|
|
560
|
+
parentId: z3.string().optional(),
|
|
561
|
+
objective: z3.string().nullable(),
|
|
562
|
+
state: z3.enum(["active", "blocked", "waiting_external"]),
|
|
563
|
+
blockedOn: z3.array(z3.string().uuid()),
|
|
564
|
+
updatedAt: z3.string()
|
|
547
565
|
})).optional(),
|
|
548
566
|
/** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
|
|
549
567
|
* if blocked, or at a time that has passed). Re-surfaced every sweep until you
|
|
550
568
|
* fulfill one by calling contact on its parentId. */
|
|
551
569
|
/** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
|
|
552
570
|
* reach. Same array the contact/await replies carry — one queue, every carrier. */
|
|
553
|
-
also:
|
|
554
|
-
owedCallbacks:
|
|
555
|
-
|
|
571
|
+
also: z3.array(RideAlongSchema).optional(),
|
|
572
|
+
owedCallbacks: z3.array(
|
|
573
|
+
z3.object({ parentId: z3.string(), trigger: CallbackTriggerSchema, note: z3.string() })
|
|
556
574
|
),
|
|
557
575
|
/** Work (either direction) you reported in_progress a while ago and never reported
|
|
558
576
|
* completed — likely left half-done by this session or a prior one that crashed or
|
|
559
577
|
* went idle. Report a real state (set_work_state) or continue the work. */
|
|
560
|
-
stalled:
|
|
561
|
-
|
|
578
|
+
stalled: z3.array(
|
|
579
|
+
z3.object({ parentId: z3.string(), notificationId: z3.string(), title: z3.string().nullable(), startedAt: z3.string() })
|
|
562
580
|
),
|
|
563
581
|
/** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
|
|
564
582
|
* thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
|
|
@@ -567,125 +585,132 @@ var PendingRepliesSchema = z2.object({
|
|
|
567
585
|
* ride the next turn. `items` are that thread's replies/requests in arrival order; the
|
|
568
586
|
* full payload for each is in the flat `replies`/`requests` arrays (matched by
|
|
569
587
|
* notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
|
|
570
|
-
threads:
|
|
571
|
-
|
|
572
|
-
parentId:
|
|
573
|
-
busy:
|
|
574
|
-
items:
|
|
575
|
-
|
|
576
|
-
kind:
|
|
577
|
-
notificationId:
|
|
578
|
-
at:
|
|
588
|
+
threads: z3.array(
|
|
589
|
+
z3.object({
|
|
590
|
+
parentId: z3.string(),
|
|
591
|
+
busy: z3.boolean(),
|
|
592
|
+
items: z3.array(
|
|
593
|
+
z3.object({
|
|
594
|
+
kind: z3.enum(["reply", "request"]),
|
|
595
|
+
notificationId: z3.string(),
|
|
596
|
+
at: z3.string()
|
|
579
597
|
})
|
|
580
598
|
)
|
|
581
599
|
})
|
|
582
600
|
)
|
|
583
601
|
});
|
|
584
|
-
var NotifyResponseSchema =
|
|
585
|
-
notificationId:
|
|
586
|
-
workId:
|
|
587
|
-
decisionId:
|
|
602
|
+
var NotifyResponseSchema = z3.object({
|
|
603
|
+
notificationId: z3.string(),
|
|
604
|
+
workId: z3.string().uuid().optional(),
|
|
605
|
+
decisionId: z3.string().uuid().optional(),
|
|
588
606
|
status: NotifyStatusSchema,
|
|
589
|
-
createdAt:
|
|
607
|
+
createdAt: z3.string().datetime(),
|
|
590
608
|
answer: UserAnswerSchema.optional(),
|
|
591
|
-
answeredAt:
|
|
609
|
+
answeredAt: z3.string().datetime().optional(),
|
|
592
610
|
/** Ride-alongs for THIS agent — pending work it should pick up when it's done with
|
|
593
611
|
* what it came for. Present on any reply, because an unwakeable agent's only
|
|
594
612
|
* reliable moment is one it initiated. Absent/empty = nothing owed. */
|
|
595
|
-
also:
|
|
613
|
+
also: z3.array(RideAlongSchema).optional()
|
|
596
614
|
});
|
|
597
|
-
var NotifyPlanUnitSchema =
|
|
598
|
-
notificationId:
|
|
615
|
+
var NotifyPlanUnitSchema = z3.object({
|
|
616
|
+
notificationId: z3.string(),
|
|
599
617
|
/** The unit's own heading, so the agent can tell which of its paragraphs this became. */
|
|
600
|
-
title:
|
|
618
|
+
title: z3.string(),
|
|
601
619
|
/** How loudly this unit was arbitrated to arrive — per unit, which is the point of units. */
|
|
602
620
|
level: NotifyLevelSchema,
|
|
603
621
|
/** Answered from something the user already decided: nobody is interrupted, and a trail card
|
|
604
622
|
* says so. The agent should not wait on this one. */
|
|
605
|
-
settled:
|
|
623
|
+
settled: z3.literal(true).optional(),
|
|
606
624
|
/** What this unit would need to be answerable and does not carry (#894). A PROPOSAL to the
|
|
607
625
|
* agent — nothing here changed the ask, and ignoring it costs nothing. */
|
|
608
|
-
needs:
|
|
626
|
+
needs: z3.array(z3.enum(["options", "visuals"])).optional(),
|
|
609
627
|
/** The SHAPE the broker would give this unit, for the agent to ratify (#886/#894). The
|
|
610
628
|
* split layer reads prose and can see that a paragraph is a yes/no or a pick-one — but a
|
|
611
629
|
* broker that DECIDES that destroys the only fact separating a statement from a real ask
|
|
612
630
|
* (#731), so it is offered, never applied: the unit is stored `text` until the agent
|
|
613
631
|
* confirms the shape (POST /notify/:id/confirm). Ignoring it costs nothing. */
|
|
614
|
-
proposal:
|
|
632
|
+
proposal: z3.object({
|
|
615
633
|
select: SelectShapeSchema,
|
|
616
|
-
options:
|
|
634
|
+
options: z3.array(z3.object({ label: z3.string().min(1) })).optional()
|
|
617
635
|
}).optional(),
|
|
618
636
|
/** What the broker READ this unit as wanting from the human (#952 layer 2): a `decision`
|
|
619
637
|
* between alternatives, an `approval` the agent is blocked on, or `knowledge` it just
|
|
620
638
|
* needs to know. Reported so the agent can correct a misread the same way it ratifies a
|
|
621
639
|
* shape — the read RAISES (a decision always asks) and never silences a question the
|
|
622
640
|
* agent declared (#731, #923). */
|
|
623
|
-
wants:
|
|
641
|
+
wants: z3.enum(["decision", "approval", "knowledge"]).optional()
|
|
624
642
|
});
|
|
625
|
-
var NotifyPlanSchema =
|
|
626
|
-
units:
|
|
643
|
+
var NotifyPlanSchema = z3.object({
|
|
644
|
+
units: z3.array(NotifyPlanUnitSchema),
|
|
627
645
|
/** Which unit is this arrival's ONE interruption (units-design.md D23). Absent means nobody
|
|
628
646
|
* was interrupted — every unit was either settled or quiet enough to sit in the inbox. */
|
|
629
|
-
speaks:
|
|
647
|
+
speaks: z3.string().optional()
|
|
630
648
|
});
|
|
631
|
-
var UserResponseSchema =
|
|
632
|
-
requestId:
|
|
649
|
+
var UserResponseSchema = z3.object({
|
|
650
|
+
requestId: z3.string(),
|
|
633
651
|
answer: UserAnswerSchema,
|
|
634
|
-
answeredAt:
|
|
635
|
-
transcript:
|
|
652
|
+
answeredAt: z3.string().datetime(),
|
|
653
|
+
transcript: z3.string().optional(),
|
|
636
654
|
/** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the item was
|
|
637
655
|
* E2EE-sealed. Present → the server persists it opaquely and branches status on `ignored`;
|
|
638
656
|
* the plaintext `answer` is a placeholder (`{ kind: "ignored" }`) the server ignores for a
|
|
639
657
|
* sealed row. Absent = today's plaintext answer, unchanged. */
|
|
640
|
-
sealed:
|
|
658
|
+
sealed: z3.lazy(() => SealedAnswerSchema).optional(),
|
|
641
659
|
/** Coverage report (#396): which of the ask's declared `points` were addressed. */
|
|
642
|
-
covered:
|
|
643
|
-
});
|
|
644
|
-
var VoiceKeySchema =
|
|
645
|
-
var AgendaTurnSchema =
|
|
660
|
+
covered: z3.array(z3.string()).optional()
|
|
661
|
+
});
|
|
662
|
+
var VoiceKeySchema = z3.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
663
|
+
var AgendaTurnSchema = z3.object({
|
|
664
|
+
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
665
|
+
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
666
|
+
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
667
|
+
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
668
|
+
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
669
|
+
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
670
|
+
id: z3.string().optional(),
|
|
646
671
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
647
672
|
* the planner declares duplicates instead of asking them twice. */
|
|
648
|
-
coveredIds:
|
|
673
|
+
coveredIds: z3.array(z3.string()).optional(),
|
|
649
674
|
/** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
|
|
650
675
|
* line went out on 2026-07-28 and the caller could not answer it at all. */
|
|
651
|
-
info:
|
|
652
|
-
question:
|
|
676
|
+
info: z3.array(z3.string().min(1)).max(3).default([]),
|
|
677
|
+
question: z3.string().min(1).nullable(),
|
|
653
678
|
/** True on the one turn carrying the agent's own declared question. */
|
|
654
|
-
asks:
|
|
679
|
+
asks: z3.boolean().optional(),
|
|
655
680
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
656
681
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
657
682
|
* which route nothing. */
|
|
658
|
-
claimId:
|
|
683
|
+
claimId: z3.string().optional(),
|
|
659
684
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
660
|
-
voice:
|
|
685
|
+
voice: z3.string().optional(),
|
|
661
686
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
662
687
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
663
688
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
664
689
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
665
|
-
agent:
|
|
690
|
+
agent: z3.string().optional(),
|
|
666
691
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
667
692
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
668
693
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
669
694
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
670
695
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
671
|
-
agentId:
|
|
696
|
+
agentId: z3.string().optional(),
|
|
672
697
|
select: SelectShapeSchema.optional(),
|
|
673
|
-
options:
|
|
698
|
+
options: z3.array(OptionSchema.omit({ id: true })).optional(),
|
|
674
699
|
/** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
|
|
675
700
|
* seconds the floor stays open after this turn speaks. Absent = the bot's defaults
|
|
676
701
|
* (the beat for context, the answer window for asks). Clamped bot-side. */
|
|
677
|
-
pace:
|
|
702
|
+
pace: z3.number().positive().optional(),
|
|
678
703
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
679
704
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
680
705
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
681
|
-
blocking:
|
|
706
|
+
blocking: z3.boolean().optional()
|
|
682
707
|
});
|
|
683
708
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
684
|
-
var InboxItemSchema =
|
|
685
|
-
id:
|
|
709
|
+
var InboxItemSchema = z3.object({
|
|
710
|
+
id: z3.string(),
|
|
686
711
|
/** The conversation thread + connection this item lives on. Present on the replied
|
|
687
712
|
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
688
|
-
parentId:
|
|
713
|
+
parentId: z3.string().optional(),
|
|
689
714
|
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
690
715
|
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
691
716
|
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
@@ -693,13 +718,13 @@ var InboxItemSchema = z2.object({
|
|
|
693
718
|
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
694
719
|
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
695
720
|
* thread, which is what the client did for all rows until now. */
|
|
696
|
-
askId:
|
|
721
|
+
askId: z3.string().optional(),
|
|
697
722
|
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
698
723
|
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
699
724
|
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
700
725
|
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
701
726
|
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
702
|
-
seq:
|
|
727
|
+
seq: z3.number().int().optional(),
|
|
703
728
|
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
704
729
|
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
705
730
|
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
@@ -708,13 +733,13 @@ var InboxItemSchema = z2.object({
|
|
|
708
733
|
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
709
734
|
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
710
735
|
* the one part of its own arrival a single row cannot answer. */
|
|
711
|
-
units:
|
|
712
|
-
tokenId:
|
|
736
|
+
units: z3.number().int().positive().optional(),
|
|
737
|
+
tokenId: z3.string().optional(),
|
|
713
738
|
status: NotifyStatusSchema,
|
|
714
739
|
context: ContextSchema,
|
|
715
|
-
options:
|
|
740
|
+
options: z3.array(OptionSchema).optional(),
|
|
716
741
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
717
|
-
points:
|
|
742
|
+
points: z3.array(z3.string()).optional(),
|
|
718
743
|
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
719
744
|
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
720
745
|
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
@@ -723,12 +748,12 @@ var InboxItemSchema = z2.object({
|
|
|
723
748
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
724
749
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
725
750
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
726
|
-
asks:
|
|
751
|
+
asks: z3.boolean().optional(),
|
|
727
752
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
728
753
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
729
754
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
730
755
|
* which must never CLAIM stalled. */
|
|
731
|
-
lastSeenAt:
|
|
756
|
+
lastSeenAt: z3.string().optional(),
|
|
732
757
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
733
758
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
734
759
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -738,53 +763,53 @@ var InboxItemSchema = z2.object({
|
|
|
738
763
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
739
764
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
740
765
|
* `createdAt`. */
|
|
741
|
-
agentStateAt:
|
|
742
|
-
agenda:
|
|
743
|
-
visuals:
|
|
766
|
+
agentStateAt: z3.string().datetime().optional(),
|
|
767
|
+
agenda: z3.array(AgendaTurnSchema).optional(),
|
|
768
|
+
visuals: z3.array(VisualSchema).optional(),
|
|
744
769
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
745
770
|
* agent's suggestion, or a default silly name). */
|
|
746
|
-
name:
|
|
771
|
+
name: z3.string(),
|
|
747
772
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
748
773
|
voice: VoiceKeySchema.optional(),
|
|
749
|
-
repo:
|
|
750
|
-
branch:
|
|
751
|
-
createdAt:
|
|
752
|
-
snoozedUntil:
|
|
774
|
+
repo: z3.string().optional(),
|
|
775
|
+
branch: z3.string().optional(),
|
|
776
|
+
createdAt: z3.string().datetime(),
|
|
777
|
+
snoozedUntil: z3.string().datetime().optional(),
|
|
753
778
|
agentState: AgentStateSchema.default("idle"),
|
|
754
779
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
755
780
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
756
781
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
757
|
-
turn:
|
|
782
|
+
turn: z3.enum(["you", "agent"]).default("you"),
|
|
758
783
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
759
784
|
* the agent (provider-agnostic; set server-side). Absent = no hard error, though the
|
|
760
785
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
761
|
-
error:
|
|
762
|
-
clarifies:
|
|
786
|
+
error: z3.string().optional(),
|
|
787
|
+
clarifies: z3.string().optional(),
|
|
763
788
|
/** The ring ladder ran out while this was still pending — we tried to reach you and
|
|
764
789
|
* STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
|
|
765
790
|
* agent with nothing to say, which the roster drew identically until now: "nothing to
|
|
766
791
|
* say" and "gave up saying it" are opposite situations wearing the same face
|
|
767
792
|
* (navigation-design.md, gap 1). False for anything that never rang. */
|
|
768
|
-
gaveUp:
|
|
793
|
+
gaveUp: z3.boolean().default(false),
|
|
769
794
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
770
795
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
771
796
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
772
797
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
773
798
|
* feedback ABOUT. */
|
|
774
|
-
why:
|
|
799
|
+
why: z3.object({
|
|
775
800
|
asked: NotifyLevelSchema,
|
|
776
801
|
got: NotifyLevelSchema,
|
|
777
|
-
because:
|
|
778
|
-
line:
|
|
802
|
+
because: z3.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
803
|
+
line: z3.string()
|
|
779
804
|
}).optional(),
|
|
780
|
-
select:
|
|
781
|
-
confirmStyle:
|
|
805
|
+
select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
806
|
+
confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
|
|
782
807
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
783
808
|
),
|
|
784
809
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
785
810
|
* urgency (see the main README's "premier use case" + notify/states.md). Drives the
|
|
786
811
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
787
|
-
blocking:
|
|
812
|
+
blocking: z3.boolean().default(false),
|
|
788
813
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
789
814
|
answer: UserAnswerSchema.optional(),
|
|
790
815
|
/** E2EE (text lane): the sealed content this item carries when the pairing is E2EE, in
|
|
@@ -793,39 +818,53 @@ var InboxItemSchema = z2.object({
|
|
|
793
818
|
* NotifyRequest.envelope. The native app decrypts it locally via openNotification;
|
|
794
819
|
* web / an un-enrolled device can't and renders a locked placeholder. Absent = today's
|
|
795
820
|
* plaintext item (context carries the cleartext), so plaintext items are unchanged. */
|
|
796
|
-
envelope:
|
|
797
|
-
context:
|
|
798
|
-
options:
|
|
799
|
-
visuals:
|
|
821
|
+
envelope: z3.object({
|
|
822
|
+
context: z3.lazy(() => EnvelopeSchema).optional(),
|
|
823
|
+
options: z3.lazy(() => EnvelopeSchema).optional(),
|
|
824
|
+
visuals: z3.lazy(() => EnvelopeSchema).optional()
|
|
800
825
|
}).optional(),
|
|
801
826
|
/** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
|
|
802
827
|
* sender the phone replies to). Sourced server-side from this item's pairing credential.
|
|
803
828
|
* Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
|
|
804
|
-
agentX25519:
|
|
829
|
+
agentX25519: z3.string().optional(),
|
|
830
|
+
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
831
|
+
* that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
|
|
832
|
+
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
833
|
+
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
834
|
+
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
835
|
+
communication: z3.object({
|
|
836
|
+
deliveryId: z3.string(),
|
|
837
|
+
kind: z3.enum(["notification", "call"]),
|
|
838
|
+
entryId: z3.string(),
|
|
839
|
+
goalIds: z3.array(z3.string()),
|
|
840
|
+
decisionNeedId: z3.string().optional(),
|
|
841
|
+
sealed: z3.boolean(),
|
|
842
|
+
goalState: z3.string().optional()
|
|
843
|
+
}).optional()
|
|
805
844
|
});
|
|
806
|
-
var SnoozeRequestSchema =
|
|
807
|
-
requestId:
|
|
808
|
-
until:
|
|
845
|
+
var SnoozeRequestSchema = z3.object({
|
|
846
|
+
requestId: z3.string(),
|
|
847
|
+
until: z3.string().datetime()
|
|
809
848
|
});
|
|
810
849
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
811
|
-
var PushTokenSchema =
|
|
812
|
-
voipToken:
|
|
813
|
-
alertToken:
|
|
814
|
-
fcmToken:
|
|
815
|
-
platform:
|
|
850
|
+
var PushTokenSchema = z3.object({
|
|
851
|
+
voipToken: z3.string().min(1).optional(),
|
|
852
|
+
alertToken: z3.string().min(1).optional(),
|
|
853
|
+
fcmToken: z3.string().min(1).optional(),
|
|
854
|
+
platform: z3.enum(["ios", "android"])
|
|
816
855
|
}).superRefine((v, ctx) => {
|
|
817
856
|
if (v.platform !== "ios") return;
|
|
818
857
|
for (const field of ["voipToken", "alertToken"]) {
|
|
819
858
|
const token = v[field];
|
|
820
859
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
821
860
|
ctx.addIssue({
|
|
822
|
-
code:
|
|
861
|
+
code: z3.ZodIssueCode.custom,
|
|
823
862
|
path: [field],
|
|
824
863
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
825
864
|
});
|
|
826
865
|
}
|
|
827
866
|
});
|
|
828
|
-
var MissedCallSchema =
|
|
867
|
+
var MissedCallSchema = z3.enum([
|
|
829
868
|
"retry_10m",
|
|
830
869
|
"retry_30m",
|
|
831
870
|
"retry_60m",
|
|
@@ -835,32 +874,32 @@ var MissedCallSchema = z2.enum([
|
|
|
835
874
|
"inbox",
|
|
836
875
|
"dismiss"
|
|
837
876
|
]);
|
|
838
|
-
var BrokerTuningSchema =
|
|
877
|
+
var BrokerTuningSchema = z3.object({
|
|
839
878
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
840
|
-
ackVerbosity:
|
|
879
|
+
ackVerbosity: z3.enum(["normal", "none"]).optional(),
|
|
841
880
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
842
881
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
843
|
-
clarifyEagerness:
|
|
882
|
+
clarifyEagerness: z3.enum(["low", "normal", "high"]).optional(),
|
|
844
883
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
845
|
-
phrasebook:
|
|
884
|
+
phrasebook: z3.array(z3.object({ say: z3.string().min(1).max(60), mean: z3.string().min(1).max(120) })).max(24).optional(),
|
|
846
885
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
847
886
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
848
887
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
849
888
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
850
889
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
851
|
-
language:
|
|
890
|
+
language: z3.enum(["en", "es"]).optional()
|
|
852
891
|
});
|
|
853
|
-
var UserSettingsSchema =
|
|
854
|
-
permissions:
|
|
855
|
-
call:
|
|
856
|
-
banner:
|
|
857
|
-
push:
|
|
892
|
+
var UserSettingsSchema = z3.object({
|
|
893
|
+
permissions: z3.object({
|
|
894
|
+
call: z3.boolean(),
|
|
895
|
+
banner: z3.boolean(),
|
|
896
|
+
push: z3.boolean()
|
|
858
897
|
}),
|
|
859
|
-
sessionMode:
|
|
860
|
-
silentPush:
|
|
861
|
-
autoCallback:
|
|
898
|
+
sessionMode: z3.enum(["default", "all_calls", "silent"]),
|
|
899
|
+
silentPush: z3.boolean(),
|
|
900
|
+
autoCallback: z3.boolean(),
|
|
862
901
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
863
|
-
improveConsent:
|
|
902
|
+
improveConsent: z3.boolean(),
|
|
864
903
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
865
904
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
866
905
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -868,7 +907,7 @@ var UserSettingsSchema = z2.object({
|
|
|
868
907
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
869
908
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
870
909
|
* write, 'hosted' on read (see store.ts). */
|
|
871
|
-
voiceMode:
|
|
910
|
+
voiceMode: z3.enum(["hosted", "on_device"]).optional(),
|
|
872
911
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
873
912
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
874
913
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -876,10 +915,10 @@ var UserSettingsSchema = z2.object({
|
|
|
876
915
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
877
916
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
878
917
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
879
|
-
voiceTuning:
|
|
918
|
+
voiceTuning: z3.record(z3.string(), z3.union([z3.number(), z3.string()])).optional(),
|
|
880
919
|
/** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
|
|
881
920
|
* defaulted — an older client PATCHing the full object must not clobber it. */
|
|
882
|
-
pstnCalls:
|
|
921
|
+
pstnCalls: z3.boolean().optional(),
|
|
883
922
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
884
923
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
885
924
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -888,59 +927,59 @@ var UserSettingsSchema = z2.object({
|
|
|
888
927
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
889
928
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
890
929
|
* call — honest about what we know. */
|
|
891
|
-
timezone:
|
|
930
|
+
timezone: z3.string().min(1).max(64).optional(),
|
|
892
931
|
/** Account E2EE state (text lane): 'off' (default) = today's plaintext; 'on' =
|
|
893
932
|
* content is sealed end-to-end between the local agent and the phone. Like
|
|
894
933
|
* voiceMode, OPTIONAL and NOT defaulted so a stale client PATCHing the full
|
|
895
934
|
* settings object without it can't silently flip the account's E2EE state.
|
|
896
935
|
* Absent = leave unchanged on write, 'off' on read (see store.ts). The demo
|
|
897
936
|
* account is plaintext by construction and refuses any non-'off' value. */
|
|
898
|
-
e2eeMode:
|
|
937
|
+
e2eeMode: z3.enum(["off", "on"]).optional(),
|
|
899
938
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
900
939
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
901
940
|
broker: BrokerTuningSchema.optional()
|
|
902
941
|
});
|
|
903
|
-
var HistoryItemSchema =
|
|
904
|
-
id:
|
|
905
|
-
parentId:
|
|
942
|
+
var HistoryItemSchema = z3.object({
|
|
943
|
+
id: z3.string(),
|
|
944
|
+
parentId: z3.string(),
|
|
906
945
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
907
|
-
initiator:
|
|
908
|
-
title:
|
|
946
|
+
initiator: z3.enum(["user", "agent"]),
|
|
947
|
+
title: z3.string(),
|
|
909
948
|
/** The agent on the other end (its name). */
|
|
910
|
-
name:
|
|
911
|
-
createdAt:
|
|
949
|
+
name: z3.string(),
|
|
950
|
+
createdAt: z3.string(),
|
|
912
951
|
/** When the agent fetched your request (user→agent only). */
|
|
913
|
-
agentAckedAt:
|
|
952
|
+
agentAckedAt: z3.string().nullable(),
|
|
914
953
|
/** When you answered the agent's notification (agent→user only). */
|
|
915
|
-
humanAckedAt:
|
|
954
|
+
humanAckedAt: z3.string().nullable()
|
|
916
955
|
});
|
|
917
956
|
var ACTIVITY_LINES = 2;
|
|
918
957
|
var ACTIVITY_LINE_MAX = 80;
|
|
919
|
-
var AgentActivitySchema =
|
|
958
|
+
var AgentActivitySchema = z3.object({
|
|
920
959
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
921
|
-
lines:
|
|
960
|
+
lines: z3.array(z3.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
922
961
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
923
962
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
924
|
-
at:
|
|
963
|
+
at: z3.string().datetime()
|
|
925
964
|
});
|
|
926
|
-
var ConnectionSummarySchema =
|
|
965
|
+
var ConnectionSummarySchema = z3.object({
|
|
927
966
|
/** The connection = the agent's token id (used to address a request). */
|
|
928
|
-
id:
|
|
967
|
+
id: z3.string(),
|
|
929
968
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
930
969
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
931
970
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
932
971
|
* apps/api/src/tokens/devices-vs-agents-design.md. */
|
|
933
|
-
kind:
|
|
972
|
+
kind: z3.enum(["device", "agent"]).optional(),
|
|
934
973
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
935
974
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
936
975
|
* agents (phone-launched, provider-managed, or minted before the link existed). */
|
|
937
|
-
mintedByDevice:
|
|
938
|
-
device:
|
|
976
|
+
mintedByDevice: z3.string().nullable().optional(),
|
|
977
|
+
device: z3.string().nullable(),
|
|
939
978
|
/** The agent's display name (the single pairing name). */
|
|
940
|
-
name:
|
|
979
|
+
name: z3.string(),
|
|
941
980
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
942
981
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
943
|
-
provider:
|
|
982
|
+
provider: z3.string().nullable(),
|
|
944
983
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
945
984
|
voice: VoiceKeySchema.nullable(),
|
|
946
985
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -951,19 +990,19 @@ var ConnectionSummarySchema = z2.object({
|
|
|
951
990
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
952
991
|
* set once must not overrule a rule they set about one agent. */
|
|
953
992
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
954
|
-
createdAt:
|
|
993
|
+
createdAt: z3.string().datetime(),
|
|
955
994
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
956
995
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
957
|
-
lastContactAt:
|
|
996
|
+
lastContactAt: z3.string().datetime().nullable(),
|
|
958
997
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
959
998
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
960
|
-
lastSeenAt:
|
|
999
|
+
lastSeenAt: z3.string().datetime().nullable().optional(),
|
|
961
1000
|
/** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
|
|
962
1001
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
963
1002
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
964
|
-
runtime:
|
|
965
|
-
harnesses:
|
|
966
|
-
workspaces:
|
|
1003
|
+
runtime: z3.object({
|
|
1004
|
+
harnesses: z3.array(z3.object({ name: z3.string(), label: z3.string(), status: z3.string() })).optional(),
|
|
1005
|
+
workspaces: z3.array(z3.string()).optional()
|
|
967
1006
|
}).optional(),
|
|
968
1007
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
969
1008
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -972,119 +1011,147 @@ var ConnectionSummarySchema = z2.object({
|
|
|
972
1011
|
activity: AgentActivitySchema.optional(),
|
|
973
1012
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
974
1013
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
975
|
-
managed:
|
|
1014
|
+
managed: z3.boolean()
|
|
976
1015
|
});
|
|
977
|
-
var LedgerItemSchema =
|
|
978
|
-
var AgentLedgerSchema =
|
|
979
|
-
agent:
|
|
1016
|
+
var LedgerItemSchema = z3.object({ id: z3.string(), parentId: z3.string(), title: z3.string(), createdAt: z3.string() });
|
|
1017
|
+
var AgentLedgerSchema = z3.object({
|
|
1018
|
+
agent: z3.object({ id: z3.string(), name: z3.string(), revokedAt: z3.string().nullable() }),
|
|
980
1019
|
/** Its own questions you have not answered. */
|
|
981
|
-
asks:
|
|
1020
|
+
asks: z3.array(LedgerItemSchema),
|
|
982
1021
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
983
|
-
answered:
|
|
1022
|
+
answered: z3.array(LedgerItemSchema),
|
|
984
1023
|
/** Requests you sent it that it never took. */
|
|
985
|
-
requests:
|
|
986
|
-
goals:
|
|
987
|
-
callbacks:
|
|
1024
|
+
requests: z3.array(LedgerItemSchema),
|
|
1025
|
+
goals: z3.array(z3.object({ id: z3.string(), outcome: z3.string(), state: z3.string() })),
|
|
1026
|
+
callbacks: z3.array(z3.object({ id: z3.string(), parentId: z3.string(), trigger: z3.string(), note: z3.string(), dueAt: z3.string().nullable() }))
|
|
988
1027
|
});
|
|
989
|
-
var ReassignResultSchema =
|
|
990
|
-
moved:
|
|
991
|
-
parentId:
|
|
1028
|
+
var ReassignResultSchema = z3.object({
|
|
1029
|
+
moved: z3.object({ asks: z3.number(), answered: z3.number(), requests: z3.number(), goals: z3.number(), callbacks: z3.number() }),
|
|
1030
|
+
parentId: z3.string().nullable()
|
|
992
1031
|
});
|
|
993
|
-
var MoveRingSchema =
|
|
994
|
-
var MoveSchema =
|
|
995
|
-
id:
|
|
1032
|
+
var MoveRingSchema = z3.enum(["home", "travels", "retired", "quarantined"]);
|
|
1033
|
+
var MoveSchema = z3.object({
|
|
1034
|
+
id: z3.string(),
|
|
996
1035
|
/** The reusable question, as distill normalized it. */
|
|
997
|
-
question:
|
|
1036
|
+
question: z3.string(),
|
|
998
1037
|
/** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
|
|
999
|
-
answer:
|
|
1038
|
+
answer: z3.string(),
|
|
1000
1039
|
/** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
|
|
1001
1040
|
* told so, and the ruling only derives essentially the same question in the same scope. */
|
|
1002
|
-
rationale:
|
|
1041
|
+
rationale: z3.string().nullable(),
|
|
1003
1042
|
/** Where the ruling lives: a repo/workspace, or 'global'. */
|
|
1004
|
-
scope:
|
|
1043
|
+
scope: z3.string(),
|
|
1005
1044
|
ring: MoveRingSchema,
|
|
1006
1045
|
/** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
|
|
1007
|
-
pinned:
|
|
1046
|
+
pinned: z3.boolean(),
|
|
1008
1047
|
/** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
|
|
1009
1048
|
* break is visible instead of a pin silently disappearing. */
|
|
1010
|
-
pinBroken:
|
|
1049
|
+
pinBroken: z3.boolean(),
|
|
1011
1050
|
/** When the ruling was distilled. */
|
|
1012
|
-
learnedAt:
|
|
1051
|
+
learnedAt: z3.string(),
|
|
1013
1052
|
/** Last time it answered an ask. Null = never fired. */
|
|
1014
|
-
lastUsedAt:
|
|
1053
|
+
lastUsedAt: z3.string().nullable(),
|
|
1015
1054
|
/** How many asks it has answered. Instrumentation — deliberately NOT an input to the
|
|
1016
1055
|
* evidence curve: firing says the question keeps arising, not that the ruling is right. */
|
|
1017
|
-
usedCount:
|
|
1056
|
+
usedCount: z3.number(),
|
|
1018
1057
|
/** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
|
|
1019
|
-
confirms:
|
|
1058
|
+
confirms: z3.number(),
|
|
1020
1059
|
/** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
|
|
1021
1060
|
* Linear and priced above the entire confirmation budget, so any full counter wins. */
|
|
1022
|
-
counters:
|
|
1061
|
+
counters: z3.number(),
|
|
1023
1062
|
/** The agent that asked the question this move came from, when known. Null for a move
|
|
1024
1063
|
* distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
|
|
1025
|
-
learnedFrom:
|
|
1064
|
+
learnedFrom: z3.object({ id: z3.string(), name: z3.string() }).nullable()
|
|
1026
1065
|
});
|
|
1027
|
-
var CreateRequestSchema =
|
|
1066
|
+
var CreateRequestSchema = z3.object({
|
|
1028
1067
|
/** The connection (token id) to send to, from GET /api/tokens. */
|
|
1029
|
-
tokenId:
|
|
1068
|
+
tokenId: z3.string(),
|
|
1030
1069
|
/** The user's message to the agent. */
|
|
1031
|
-
text:
|
|
1070
|
+
text: z3.string().min(1),
|
|
1032
1071
|
/** Land the request on an existing conversation thread (History → "Continue")
|
|
1033
1072
|
* instead of minting a fresh one. Must belong to the requesting user. */
|
|
1034
|
-
parentId:
|
|
1073
|
+
parentId: z3.string().optional(),
|
|
1035
1074
|
/** Point the agent at a past conversation (possibly with a different agent) as
|
|
1036
1075
|
* starting context (History → "New session from this"). A reference, not a copy —
|
|
1037
1076
|
* the agent reads it via get_thread. Must belong to the requesting user. */
|
|
1038
|
-
contextParentId:
|
|
1039
|
-
});
|
|
1040
|
-
var
|
|
1041
|
-
/**
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
|
|
1045
|
-
/**
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
/**
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
var
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1077
|
+
contextParentId: z3.string().optional()
|
|
1078
|
+
});
|
|
1079
|
+
var QueueQuestionSchema = z3.object({
|
|
1080
|
+
/** The decision need's id — what an answer is accepted against. */
|
|
1081
|
+
id: z3.string(),
|
|
1082
|
+
/** The words that were asked, from the request Entry that asked them. */
|
|
1083
|
+
question: z3.string(),
|
|
1084
|
+
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
1085
|
+
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
1086
|
+
* can answer. */
|
|
1087
|
+
deliveryId: z3.string().nullable().default(null),
|
|
1088
|
+
/** The Entry the ruling is about. */
|
|
1089
|
+
aboutId: z3.string().nullable().default(null),
|
|
1090
|
+
/** Empty for a free-text question. */
|
|
1091
|
+
options: z3.array(OptionSchema).default([]),
|
|
1092
|
+
select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
1093
|
+
askedAt: z3.string(),
|
|
1094
|
+
/** Null while the question is open — which is how the page tells the two apart. */
|
|
1095
|
+
answeredAt: z3.string().nullable().default(null),
|
|
1096
|
+
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
1097
|
+
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
1098
|
+
* for a settled question whose reply carried nothing readable. */
|
|
1099
|
+
answer: z3.string().nullable().default(null)
|
|
1100
|
+
});
|
|
1101
|
+
var QueueItemSchema = z3.object({
|
|
1102
|
+
id: z3.string(),
|
|
1103
|
+
/** One-line headline — the first sentence of the outcome. */
|
|
1104
|
+
title: z3.string(),
|
|
1105
|
+
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
1106
|
+
intent: z3.string(),
|
|
1107
|
+
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
1108
|
+
state: z3.string(),
|
|
1109
|
+
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
1110
|
+
assignee: z3.string().nullable().default(null),
|
|
1111
|
+
/** What the agent last said it was doing; null if it has said nothing. */
|
|
1112
|
+
progress: z3.string().nullable().default(null),
|
|
1113
|
+
reviewPending: z3.boolean().default(false),
|
|
1114
|
+
dueAt: z3.string().nullable().default(null),
|
|
1115
|
+
/** The Goal this one was opened under; null at the root. */
|
|
1116
|
+
parentGoalId: z3.string().nullable().default(null),
|
|
1117
|
+
/** Goals opened under this one — only those the same list holds. */
|
|
1118
|
+
childGoalIds: z3.array(z3.string()).default([]),
|
|
1119
|
+
/** Goals this one waits on (start or finish gates). */
|
|
1120
|
+
dependencyGoalIds: z3.array(z3.string()).default([]),
|
|
1121
|
+
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
1122
|
+
blocked: z3.boolean().default(false),
|
|
1123
|
+
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
1124
|
+
questions: z3.array(QueueQuestionSchema).default([]),
|
|
1125
|
+
createdAt: z3.string(),
|
|
1126
|
+
updatedAt: z3.string().nullable().default(null)
|
|
1127
|
+
});
|
|
1128
|
+
var NoteSourceSchema = z3.enum(["app", "call"]);
|
|
1129
|
+
var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
|
|
1130
|
+
var NoteRepeatSchema = z3.enum(["once", "until_done"]);
|
|
1131
|
+
var DecisionSchema = z3.object({
|
|
1132
|
+
id: z3.string(),
|
|
1066
1133
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
1067
1134
|
* extensibility seam — any conversation can accrue decisions). */
|
|
1068
|
-
noteId:
|
|
1135
|
+
noteId: z3.string().nullable(),
|
|
1069
1136
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
1070
|
-
question:
|
|
1137
|
+
question: z3.string(),
|
|
1071
1138
|
/** The user's ruling; null while the question is open. */
|
|
1072
|
-
answer:
|
|
1073
|
-
decidedAt:
|
|
1074
|
-
createdAt:
|
|
1139
|
+
answer: z3.string().nullable(),
|
|
1140
|
+
decidedAt: z3.string().nullable(),
|
|
1141
|
+
createdAt: z3.string()
|
|
1075
1142
|
});
|
|
1076
|
-
var NoteSchema =
|
|
1077
|
-
id:
|
|
1143
|
+
var NoteSchema = z3.object({
|
|
1144
|
+
id: z3.string(),
|
|
1078
1145
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
1079
|
-
title:
|
|
1146
|
+
title: z3.string(),
|
|
1080
1147
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
1081
|
-
intent:
|
|
1148
|
+
intent: z3.string(),
|
|
1082
1149
|
source: NoteSourceSchema,
|
|
1083
1150
|
status: NoteStatusSchema,
|
|
1084
1151
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
1085
|
-
assignee:
|
|
1152
|
+
assignee: z3.string().nullable(),
|
|
1086
1153
|
/** The request thread minted at assignment; null until assigned. */
|
|
1087
|
-
parentId:
|
|
1154
|
+
parentId: z3.string().nullable(),
|
|
1088
1155
|
/** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
1089
1156
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
1090
1157
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -1093,202 +1160,214 @@ var NoteSchema = z2.object({
|
|
|
1093
1160
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
1094
1161
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
1095
1162
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
1096
|
-
dueAt:
|
|
1163
|
+
dueAt: z3.string().nullable().default(null),
|
|
1097
1164
|
repeat: NoteRepeatSchema.default("once"),
|
|
1098
1165
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
1099
|
-
rides:
|
|
1100
|
-
lastRideAt:
|
|
1101
|
-
createdAt:
|
|
1166
|
+
rides: z3.number().int().default(0),
|
|
1167
|
+
lastRideAt: z3.string().nullable().default(null),
|
|
1168
|
+
createdAt: z3.string()
|
|
1102
1169
|
});
|
|
1103
|
-
var CreateNoteSchema =
|
|
1170
|
+
var CreateNoteSchema = z3.object({
|
|
1104
1171
|
/** The intent, in the user's own words. Stored verbatim; the broker only titles it. */
|
|
1105
|
-
text:
|
|
1172
|
+
text: z3.string().min(1).max(4e3),
|
|
1106
1173
|
/** Capture it as a REMINDER — a note assigned to the user themselves, which rides
|
|
1107
1174
|
* their next call instead of being handed to an agent. Everything else about the
|
|
1108
1175
|
* note is identical; this is the one parameter that separates the two. */
|
|
1109
|
-
forMe:
|
|
1176
|
+
forMe: z3.boolean().optional(),
|
|
1110
1177
|
/** The not-before, when the user already said one. Absent = the very next call. */
|
|
1111
|
-
dueAt:
|
|
1178
|
+
dueAt: z3.string().datetime().optional(),
|
|
1112
1179
|
repeat: NoteRepeatSchema.optional()
|
|
1113
1180
|
});
|
|
1114
|
-
var RecordDecisionSchema =
|
|
1181
|
+
var RecordDecisionSchema = z3.object({
|
|
1115
1182
|
/** An open decision (from /clarify) to answer. */
|
|
1116
|
-
decisionId:
|
|
1183
|
+
decisionId: z3.string().optional(),
|
|
1117
1184
|
/** For an ad-hoc decision: what was ambiguous. Required without `decisionId`. */
|
|
1118
|
-
question:
|
|
1185
|
+
question: z3.string().min(1).max(500).optional(),
|
|
1119
1186
|
/** The ruling. */
|
|
1120
|
-
answer:
|
|
1187
|
+
answer: z3.string().min(1).max(2e3)
|
|
1121
1188
|
}).refine((d) => d.decisionId || d.question, { message: "decisionId or question required" });
|
|
1122
|
-
var AssignNoteSchema =
|
|
1189
|
+
var AssignNoteSchema = z3.object({
|
|
1123
1190
|
/** An EXISTING agent: token id or nickname. Omit when spawning fresh. */
|
|
1124
|
-
target:
|
|
1191
|
+
target: z3.string().min(1).optional(),
|
|
1125
1192
|
/** Spawn a NEW session for this note (companion.md §2.2): the assignee doesn't
|
|
1126
1193
|
* exist yet — mint it on a live desktop that advertises the harness+workspace,
|
|
1127
1194
|
* named after the note. The brief arrives as its opening request. */
|
|
1128
|
-
spawn:
|
|
1129
|
-
hostTokenId:
|
|
1130
|
-
harness:
|
|
1131
|
-
workspace:
|
|
1195
|
+
spawn: z3.object({
|
|
1196
|
+
hostTokenId: z3.string().uuid(),
|
|
1197
|
+
harness: z3.string().min(1),
|
|
1198
|
+
workspace: z3.string().min(1)
|
|
1132
1199
|
}).optional()
|
|
1133
1200
|
}).refine((a) => !!a.target !== !!a.spawn, { message: "exactly one of target or spawn" });
|
|
1134
|
-
var TriageVerdictSchema =
|
|
1135
|
-
var TriageItemSchema =
|
|
1136
|
-
noteId:
|
|
1201
|
+
var TriageVerdictSchema = z3.enum(["keep", "stale", "close", "assign"]);
|
|
1202
|
+
var TriageItemSchema = z3.object({
|
|
1203
|
+
noteId: z3.string(),
|
|
1137
1204
|
/** The note's headline at run time. */
|
|
1138
|
-
title:
|
|
1205
|
+
title: z3.string(),
|
|
1139
1206
|
/** WHY, in one short human line, evidence first — this is read on a phone underneath
|
|
1140
1207
|
* the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
|
|
1141
1208
|
* Never a model's reasoning transcript, never an id. */
|
|
1142
|
-
why:
|
|
1209
|
+
why: z3.string()
|
|
1143
1210
|
});
|
|
1144
|
-
var TriageAssignmentSchema =
|
|
1211
|
+
var TriageAssignmentSchema = z3.object({
|
|
1145
1212
|
/** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
|
|
1146
|
-
agent:
|
|
1213
|
+
agent: z3.string(),
|
|
1147
1214
|
/** Its display name at run time (the name on the hatchling's card). Denormalized for the
|
|
1148
1215
|
* same reason as `title`: the card must render from the proposal alone. */
|
|
1149
|
-
agentName:
|
|
1150
|
-
notes:
|
|
1216
|
+
agentName: z3.string(),
|
|
1217
|
+
notes: z3.array(TriageItemSchema)
|
|
1151
1218
|
});
|
|
1152
|
-
var TriageStatusSchema =
|
|
1153
|
-
var SubmitTriageSchema =
|
|
1219
|
+
var TriageStatusSchema = z3.enum(["open", "superseded", "dismissed"]);
|
|
1220
|
+
var SubmitTriageSchema = z3.object({
|
|
1154
1221
|
/** Which runtime judged: "ollama" (inference never left the machine) or a harness the
|
|
1155
1222
|
* user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
|
|
1156
1223
|
* so the phone can say where the content went — an unattributed privacy claim is worth
|
|
1157
1224
|
* nothing, and #1106's promise is precisely "Paigy's servers never see this". */
|
|
1158
|
-
provider:
|
|
1225
|
+
provider: z3.string().min(1).max(60),
|
|
1159
1226
|
/** The concrete model when the provider names one (an ollama tag); null otherwise. */
|
|
1160
|
-
model:
|
|
1227
|
+
model: z3.string().max(200).nullable().optional(),
|
|
1161
1228
|
/** How many open notes the run actually looked at — the denominator on the phone
|
|
1162
1229
|
* ("6 of 50"), and the honest answer to "did it read the whole queue?". */
|
|
1163
|
-
reviewed:
|
|
1164
|
-
close:
|
|
1165
|
-
stale:
|
|
1166
|
-
assign:
|
|
1230
|
+
reviewed: z3.number().int().min(0).max(1e4).default(0),
|
|
1231
|
+
close: z3.array(TriageItemSchema).max(200).default([]),
|
|
1232
|
+
stale: z3.array(TriageItemSchema).max(200).default([]),
|
|
1233
|
+
assign: z3.array(TriageAssignmentSchema).max(50).default([])
|
|
1167
1234
|
});
|
|
1168
1235
|
var TriageProposalSchema = SubmitTriageSchema.extend({
|
|
1169
|
-
id:
|
|
1170
|
-
runAt:
|
|
1236
|
+
id: z3.string(),
|
|
1237
|
+
runAt: z3.string(),
|
|
1171
1238
|
status: TriageStatusSchema,
|
|
1172
|
-
model:
|
|
1173
|
-
});
|
|
1174
|
-
var AcceptTriageSchema =
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
group:
|
|
1179
|
-
agent:
|
|
1180
|
-
noteIds:
|
|
1239
|
+
model: z3.string().nullable().default(null)
|
|
1240
|
+
});
|
|
1241
|
+
var AcceptTriageSchema = z3.discriminatedUnion("group", [
|
|
1242
|
+
z3.object({ group: z3.literal("close"), noteIds: z3.array(z3.string()).max(200).optional() }),
|
|
1243
|
+
z3.object({ group: z3.literal("stale"), noteIds: z3.array(z3.string()).max(200).optional() }),
|
|
1244
|
+
z3.object({
|
|
1245
|
+
group: z3.literal("assign"),
|
|
1246
|
+
agent: z3.string().min(1),
|
|
1247
|
+
noteIds: z3.array(z3.string()).max(200).optional()
|
|
1181
1248
|
})
|
|
1182
1249
|
]);
|
|
1183
|
-
var AcceptTriageResultSchema =
|
|
1184
|
-
accepted:
|
|
1185
|
-
failed:
|
|
1186
|
-
});
|
|
1187
|
-
var DeliveryModeSchema =
|
|
1188
|
-
var
|
|
1189
|
-
var
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
var DeliveryConfigSchema = z2.object({
|
|
1196
|
-
tokenId: z2.string(),
|
|
1250
|
+
var AcceptTriageResultSchema = z3.object({
|
|
1251
|
+
accepted: z3.array(z3.string()),
|
|
1252
|
+
failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
|
|
1253
|
+
});
|
|
1254
|
+
var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
|
|
1255
|
+
var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
|
|
1256
|
+
var OAuthStartSchema = z3.object({
|
|
1257
|
+
provider: z3.enum(["cma"]),
|
|
1258
|
+
returnTo: z3.string().min(1)
|
|
1259
|
+
});
|
|
1260
|
+
var DeliveryConfigSchema = z3.object({
|
|
1261
|
+
tokenId: z3.string(),
|
|
1197
1262
|
mode: DeliveryModeSchema,
|
|
1198
|
-
/** null when the
|
|
1199
|
-
*
|
|
1200
|
-
|
|
1201
|
-
})
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1263
|
+
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
1264
|
+
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
1265
|
+
* carries credentials; only a `poll` registration can come back with null here. */
|
|
1266
|
+
realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
|
|
1267
|
+
});
|
|
1268
|
+
var StatusSchema = z3.object({
|
|
1269
|
+
name: z3.string(),
|
|
1270
|
+
sessionMode: z3.enum(["default", "all_calls", "silent"]),
|
|
1205
1271
|
/** A phone is registered for push/ring (any push token on the account). */
|
|
1206
|
-
phone:
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
});
|
|
1221
|
-
var
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1272
|
+
phone: z3.boolean(),
|
|
1273
|
+
/** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
|
|
1274
|
+
* it never picked up. THE SAME NUMBER the harness's wake gate reads
|
|
1275
|
+
* (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
|
|
1276
|
+
* zero while the sweep sees one is two ideas of "waiting".
|
|
1277
|
+
*
|
|
1278
|
+
* Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
|
|
1279
|
+
* that nobody spawned, so a terminal session only learns of work by asking. The harness
|
|
1280
|
+
* used to paper over that by spawning a SECOND process on the identity; now it stands
|
|
1281
|
+
* back, correctly, and the person sitting at the terminal is the one who can act. A
|
|
1282
|
+
* coffee-beans request sat unread for three days.
|
|
1283
|
+
*
|
|
1284
|
+
* Optional: an older API sends no field, and the statusline then renders exactly as before. */
|
|
1285
|
+
waiting: z3.number().int().nonnegative().optional()
|
|
1286
|
+
});
|
|
1287
|
+
var EnvelopeRecipientSchema = z3.object({
|
|
1288
|
+
keyId: z3.string(),
|
|
1289
|
+
epk: z3.string(),
|
|
1290
|
+
wnonce: z3.string(),
|
|
1291
|
+
wrap: z3.string()
|
|
1292
|
+
});
|
|
1293
|
+
var EnvelopeHeaderSchema = z3.object({
|
|
1294
|
+
field: z3.enum(["context", "options", "visuals", "answer"]),
|
|
1295
|
+
kind: z3.string(),
|
|
1296
|
+
senderRole: z3.enum(["agent", "user"]),
|
|
1297
|
+
recipientKeyIds: z3.array(z3.string()),
|
|
1298
|
+
seq: z3.number().int().nonnegative()
|
|
1299
|
+
});
|
|
1300
|
+
var EnvelopeSchema = z3.object({
|
|
1301
|
+
v: z3.literal(1),
|
|
1302
|
+
alg: z3.literal("x25519-xsalsa20poly1305"),
|
|
1303
|
+
msgId: z3.string(),
|
|
1225
1304
|
hdr: EnvelopeHeaderSchema,
|
|
1226
|
-
recipients:
|
|
1227
|
-
nonce:
|
|
1228
|
-
ct:
|
|
1305
|
+
recipients: z3.array(EnvelopeRecipientSchema).min(1),
|
|
1306
|
+
nonce: z3.string(),
|
|
1307
|
+
ct: z3.string()
|
|
1229
1308
|
});
|
|
1230
|
-
var SealedAnswerSchema =
|
|
1231
|
-
ignored:
|
|
1309
|
+
var SealedAnswerSchema = z3.object({
|
|
1310
|
+
ignored: z3.boolean(),
|
|
1232
1311
|
envelope: EnvelopeSchema
|
|
1233
1312
|
});
|
|
1234
|
-
var DeviceCredentialSchema =
|
|
1235
|
-
deviceId:
|
|
1236
|
-
kind:
|
|
1237
|
-
x25519Pub:
|
|
1238
|
-
ed25519Pub:
|
|
1239
|
-
sig:
|
|
1240
|
-
});
|
|
1241
|
-
var DeviceRosterSchema =
|
|
1242
|
-
uikPub:
|
|
1243
|
-
devices:
|
|
1244
|
-
});
|
|
1245
|
-
var WakeNudgeSchema =
|
|
1246
|
-
kind:
|
|
1247
|
-
notificationId:
|
|
1248
|
-
parentId:
|
|
1249
|
-
});
|
|
1250
|
-
var PairingStatusSchema =
|
|
1251
|
-
var PairingRevealSchema =
|
|
1252
|
-
x25519:
|
|
1253
|
-
ed25519:
|
|
1254
|
-
nonce:
|
|
1255
|
-
});
|
|
1256
|
-
var DeviceCodeRequestSchema =
|
|
1313
|
+
var DeviceCredentialSchema = z3.object({
|
|
1314
|
+
deviceId: z3.string(),
|
|
1315
|
+
kind: z3.enum(["phone", "web", "agent"]),
|
|
1316
|
+
x25519Pub: z3.string(),
|
|
1317
|
+
ed25519Pub: z3.string(),
|
|
1318
|
+
sig: z3.string()
|
|
1319
|
+
});
|
|
1320
|
+
var DeviceRosterSchema = z3.object({
|
|
1321
|
+
uikPub: z3.string(),
|
|
1322
|
+
devices: z3.array(DeviceCredentialSchema)
|
|
1323
|
+
});
|
|
1324
|
+
var WakeNudgeSchema = z3.object({
|
|
1325
|
+
kind: z3.enum(["reply", "request", "callback"]),
|
|
1326
|
+
notificationId: z3.string().optional(),
|
|
1327
|
+
parentId: z3.string()
|
|
1328
|
+
});
|
|
1329
|
+
var PairingStatusSchema = z3.enum(["pending", "approved", "denied", "expired"]);
|
|
1330
|
+
var PairingRevealSchema = z3.object({
|
|
1331
|
+
x25519: z3.string(),
|
|
1332
|
+
ed25519: z3.string(),
|
|
1333
|
+
nonce: z3.string()
|
|
1334
|
+
});
|
|
1335
|
+
var DeviceCodeRequestSchema = z3.object({
|
|
1257
1336
|
/** The agent's suggested name for the pairing — the human sees it pre-filled at
|
|
1258
1337
|
* approval and can override. Optional; blank → a default silly name server-side. */
|
|
1259
|
-
suggestedName:
|
|
1338
|
+
suggestedName: z3.string().optional(),
|
|
1260
1339
|
/** Legacy alias for suggestedName (older MCPs sent `agent`). Accepted for
|
|
1261
1340
|
* back-compat; `suggestedName` wins when both are present. ponytail: drop once no
|
|
1262
1341
|
* pre-`name` MCP is in the wild. */
|
|
1263
|
-
agent:
|
|
1264
|
-
device:
|
|
1265
|
-
proto:
|
|
1342
|
+
agent: z3.union([z3.string(), z3.object({ name: z3.string().optional() })]).optional(),
|
|
1343
|
+
device: z3.string().optional(),
|
|
1344
|
+
proto: z3.string().optional(),
|
|
1266
1345
|
// compiled-in tag, e.g. "paigy-pair-v2|sas=24"
|
|
1267
|
-
commitment:
|
|
1346
|
+
commitment: z3.string().optional()
|
|
1268
1347
|
// agent's Ca
|
|
1269
1348
|
});
|
|
1270
|
-
var DeviceCodeSchema =
|
|
1271
|
-
device_code:
|
|
1272
|
-
user_code:
|
|
1273
|
-
verification_uri:
|
|
1274
|
-
verification_uri_complete:
|
|
1275
|
-
interval:
|
|
1276
|
-
expires_in:
|
|
1349
|
+
var DeviceCodeSchema = z3.object({
|
|
1350
|
+
device_code: z3.string(),
|
|
1351
|
+
user_code: z3.string(),
|
|
1352
|
+
verification_uri: z3.string().url(),
|
|
1353
|
+
verification_uri_complete: z3.string().url(),
|
|
1354
|
+
interval: z3.number(),
|
|
1355
|
+
expires_in: z3.number()
|
|
1277
1356
|
});
|
|
1278
|
-
var DeviceInfoSchema =
|
|
1279
|
-
code:
|
|
1357
|
+
var DeviceInfoSchema = z3.object({
|
|
1358
|
+
code: z3.string(),
|
|
1280
1359
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
1281
1360
|
* pre-filling the name field the human can edit. */
|
|
1282
|
-
name:
|
|
1361
|
+
name: z3.string(),
|
|
1283
1362
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
1284
1363
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
1285
1364
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
1286
1365
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
1287
|
-
agent:
|
|
1288
|
-
device:
|
|
1366
|
+
agent: z3.string().optional(),
|
|
1367
|
+
device: z3.string().nullable(),
|
|
1289
1368
|
status: PairingStatusSchema,
|
|
1290
|
-
proto:
|
|
1291
|
-
agent_commitment:
|
|
1369
|
+
proto: z3.string().nullable().optional(),
|
|
1370
|
+
agent_commitment: z3.string().nullable().optional(),
|
|
1292
1371
|
// Ca
|
|
1293
1372
|
agent_reveal: PairingRevealSchema.nullable().optional(),
|
|
1294
1373
|
// present once the agent reveals
|
|
@@ -1298,48 +1377,48 @@ var DeviceInfoSchema = z2.object({
|
|
|
1298
1377
|
// withheld until e2ee=true) can compute the SAS WITHOUT a token. Public keys — same safety
|
|
1299
1378
|
// class as agent_reveal above. Present only once the phone reveals; null otherwise.
|
|
1300
1379
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
1301
|
-
uik_pub:
|
|
1380
|
+
uik_pub: z3.string().nullable().optional()
|
|
1302
1381
|
});
|
|
1303
|
-
var DeviceTokenRequestSchema =
|
|
1304
|
-
device_code:
|
|
1382
|
+
var DeviceTokenRequestSchema = z3.object({
|
|
1383
|
+
device_code: z3.string(),
|
|
1305
1384
|
reveal: PairingRevealSchema.optional()
|
|
1306
1385
|
// agent's reveal {x25519, ed25519, nonce}
|
|
1307
1386
|
});
|
|
1308
|
-
var DeviceTokenSchema =
|
|
1309
|
-
access_token:
|
|
1387
|
+
var DeviceTokenSchema = z3.object({
|
|
1388
|
+
access_token: z3.string(),
|
|
1310
1389
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
1311
1390
|
* a default silly name). */
|
|
1312
|
-
name:
|
|
1313
|
-
device:
|
|
1391
|
+
name: z3.string(),
|
|
1392
|
+
device: z3.string().nullable(),
|
|
1314
1393
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
1315
1394
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
1316
|
-
voice:
|
|
1395
|
+
voice: z3.string().nullable().optional(),
|
|
1317
1396
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
1318
1397
|
* that survives a rename. Cached by the host's identity beat. */
|
|
1319
|
-
token_id:
|
|
1398
|
+
token_id: z3.string().nullable().optional(),
|
|
1320
1399
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
1321
1400
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
1322
1401
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
1323
1402
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
1324
|
-
workspace:
|
|
1403
|
+
workspace: z3.string().nullable().optional(),
|
|
1325
1404
|
phone_reveal: PairingRevealSchema.nullable().optional(),
|
|
1326
1405
|
// present once the phone reveals
|
|
1327
|
-
uik_pub:
|
|
1406
|
+
uik_pub: z3.string().nullable().optional()
|
|
1328
1407
|
});
|
|
1329
|
-
var DeviceCommitRequestSchema =
|
|
1330
|
-
code:
|
|
1331
|
-
commitment:
|
|
1408
|
+
var DeviceCommitRequestSchema = z3.object({
|
|
1409
|
+
code: z3.string(),
|
|
1410
|
+
commitment: z3.string()
|
|
1332
1411
|
// Cb
|
|
1333
1412
|
});
|
|
1334
|
-
var DevicePeerCommitSchema =
|
|
1335
|
-
commitment:
|
|
1413
|
+
var DevicePeerCommitSchema = z3.object({
|
|
1414
|
+
commitment: z3.string().nullable()
|
|
1336
1415
|
});
|
|
1337
|
-
var SupportRequestSchema =
|
|
1338
|
-
email:
|
|
1339
|
-
message:
|
|
1340
|
-
name:
|
|
1416
|
+
var SupportRequestSchema = z3.object({
|
|
1417
|
+
email: z3.string().email().max(320),
|
|
1418
|
+
message: z3.string().trim().min(1).max(5e3),
|
|
1419
|
+
name: z3.string().trim().max(120).optional()
|
|
1341
1420
|
});
|
|
1342
|
-
var NotificationFeedbackKindSchema =
|
|
1421
|
+
var NotificationFeedbackKindSchema = z3.enum([
|
|
1343
1422
|
"break_down",
|
|
1344
1423
|
// "This should be more than one ask — break it down."
|
|
1345
1424
|
"regenerate_options",
|
|
@@ -1353,46 +1432,26 @@ var NotificationFeedbackKindSchema = z2.enum([
|
|
|
1353
1432
|
"other"
|
|
1354
1433
|
// anything else — the note carries it.
|
|
1355
1434
|
]);
|
|
1356
|
-
var NotificationFeedbackSchema =
|
|
1357
|
-
notificationId:
|
|
1435
|
+
var NotificationFeedbackSchema = z3.object({
|
|
1436
|
+
notificationId: z3.string(),
|
|
1358
1437
|
kind: NotificationFeedbackKindSchema,
|
|
1359
1438
|
/** Optional free-text elaboration for a preset; required (non-empty) for 'other'. */
|
|
1360
|
-
note:
|
|
1439
|
+
note: z3.string().trim().max(2e3).optional()
|
|
1361
1440
|
}).superRefine((r, ctx) => {
|
|
1362
1441
|
if (r.kind === "other" && !r.note)
|
|
1363
|
-
ctx.addIssue({ code:
|
|
1442
|
+
ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["note"], message: "note is required for 'other' feedback" });
|
|
1364
1443
|
});
|
|
1365
|
-
var FeedbackResolutionSchema =
|
|
1366
|
-
var FeedbackOutcomeSchema =
|
|
1444
|
+
var FeedbackResolutionSchema = z3.enum(["broker_fixed", "sent_to_agent"]);
|
|
1445
|
+
var FeedbackOutcomeSchema = z3.object({
|
|
1367
1446
|
resolution: FeedbackResolutionSchema,
|
|
1368
1447
|
transform: TransformSchema,
|
|
1369
|
-
message:
|
|
1370
|
-
childIds:
|
|
1371
|
-
});
|
|
1372
|
-
var CONTACT_SCHEMA = contactSchemaFrom({
|
|
1373
|
-
ask: NotifyRequestFields.shape.ask,
|
|
1374
|
-
waiting: NotifyRequestFields.shape.waiting,
|
|
1375
|
-
options: NotifyRequestFields.shape.options,
|
|
1376
|
-
channel: NotifyRequestFields.shape.channel,
|
|
1377
|
-
parentId: NotifyRequestFields.shape.parentId,
|
|
1378
|
-
workId: NotifyRequestFields.shape.workId
|
|
1448
|
+
message: z3.string(),
|
|
1449
|
+
childIds: z3.array(z3.string()).optional()
|
|
1379
1450
|
});
|
|
1380
1451
|
|
|
1381
1452
|
export {
|
|
1382
1453
|
mcpInputSchema,
|
|
1383
|
-
|
|
1384
|
-
CREATE_GOAL_DESCRIPTION,
|
|
1385
|
-
CONTACT_DESCRIPTION,
|
|
1386
|
-
CHECK_REPLIES_DESCRIPTION,
|
|
1387
|
-
SCHEDULE_CALLBACK_DESCRIPTION,
|
|
1388
|
-
SEARCH_THREADS_DESCRIPTION,
|
|
1389
|
-
SET_WORK_STATE_DESCRIPTION,
|
|
1454
|
+
AGENT_TOOLS,
|
|
1390
1455
|
serverInstructions,
|
|
1391
|
-
|
|
1392
|
-
SetWorkStateSchema,
|
|
1393
|
-
ScheduleCallbackSchema,
|
|
1394
|
-
HandoffSchema,
|
|
1395
|
-
WAKE_EVENT,
|
|
1396
|
-
wakeChannel,
|
|
1397
|
-
CONTACT_SCHEMA
|
|
1456
|
+
entryWords
|
|
1398
1457
|
};
|