@athenaintel/react 0.21.2 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/chat/StatewireApprovalCard.cjs +70 -10
- package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
- package/dist/chat/StatewireApprovalCard.d.ts +12 -1
- package/dist/chat/StatewireApprovalCard.js +73 -13
- package/dist/chat/StatewireApprovalCard.js.map +1 -1
- package/dist/chat/use-statewire-approval-request.cjs +22 -1
- package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
- package/dist/chat/use-statewire-approval-request.d.ts +41 -0
- package/dist/chat/use-statewire-approval-request.js +24 -3
- package/dist/chat/use-statewire-approval-request.js.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +0 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +0 -1
- package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
- package/dist/collab/client.cjs +4 -9
- package/dist/collab/client.cjs.map +1 -1
- package/dist/collab/client.js +2 -7
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/index.cjs +23 -0
- package/dist/collab/index.cjs.map +1 -1
- package/dist/collab/index.d.ts +17 -2
- package/dist/collab/index.js +24 -1
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/mint.cjs +18 -13
- package/dist/collab/mint.cjs.map +1 -1
- package/dist/collab/mint.d.ts +10 -0
- package/dist/collab/mint.js +18 -13
- package/dist/collab/mint.js.map +1 -1
- package/dist/collab/presence-client.cjs +211 -0
- package/dist/collab/presence-client.cjs.map +1 -0
- package/dist/collab/presence-client.d.ts +60 -0
- package/dist/collab/presence-client.js +194 -0
- package/dist/collab/presence-client.js.map +1 -0
- package/dist/collab/presence-mint.cjs +68 -0
- package/dist/collab/presence-mint.cjs.map +1 -0
- package/dist/collab/presence-mint.d.ts +87 -0
- package/dist/collab/presence-mint.js +68 -0
- package/dist/collab/presence-mint.js.map +1 -0
- package/dist/collab/presence-react.cjs +114 -0
- package/dist/collab/presence-react.cjs.map +1 -0
- package/dist/collab/presence-react.d.ts +47 -0
- package/dist/collab/presence-react.js +114 -0
- package/dist/collab/presence-react.js.map +1 -0
- package/dist/collab/presence-slots.cjs +220 -0
- package/dist/collab/presence-slots.cjs.map +1 -0
- package/dist/collab/presence-slots.d.ts +119 -0
- package/dist/collab/presence-slots.js +220 -0
- package/dist/collab/presence-slots.js.map +1 -0
- package/dist/collab/token-refresh.cjs +13 -0
- package/dist/collab/token-refresh.cjs.map +1 -0
- package/dist/collab/token-refresh.d.ts +20 -0
- package/dist/collab/token-refresh.js +13 -0
- package/dist/collab/token-refresh.js.map +1 -0
- package/dist/packages/agent-runtime/src/interrupts.cjs +40 -1
- package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/interrupts.js +40 -1
- package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const PRESENCE_CONTEXT_TYPE = {
|
|
2
|
+
ASSET: "asset",
|
|
3
|
+
SESSION: "session",
|
|
4
|
+
/** Server-written only: "somewhere in that project", never which document. */
|
|
5
|
+
PROJECT: "project"
|
|
6
|
+
};
|
|
7
|
+
const MAX_PRESENCE_CONTEXTS_PER_SLOT = 64;
|
|
8
|
+
const MAX_PRESENCE_STRING_CHARS = 512;
|
|
9
|
+
const PRESENCE_ACTIVE_WINDOW_MS = 6e4;
|
|
10
|
+
const PRESENCE_IDLE_WINDOW_MS = 3e5;
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function cleanString(value) {
|
|
15
|
+
if (typeof value !== "string") return null;
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (!trimmed) return null;
|
|
18
|
+
return trimmed.slice(0, MAX_PRESENCE_STRING_CHARS);
|
|
19
|
+
}
|
|
20
|
+
function cleanEpochMs(value) {
|
|
21
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function cleanContextType(value) {
|
|
25
|
+
if (value === PRESENCE_CONTEXT_TYPE.ASSET || value === PRESENCE_CONTEXT_TYPE.SESSION || value === PRESENCE_CONTEXT_TYPE.PROJECT) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function cleanStringList(value) {
|
|
31
|
+
if (!Array.isArray(value)) return [];
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const item of value) {
|
|
34
|
+
const cleaned = cleanString(item);
|
|
35
|
+
if (cleaned === null || out.includes(cleaned)) continue;
|
|
36
|
+
out.push(cleaned);
|
|
37
|
+
if (out.length >= MAX_PRESENCE_CONTEXTS_PER_SLOT) break;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function parseContext(raw) {
|
|
42
|
+
if (!isRecord(raw)) return null;
|
|
43
|
+
const type = cleanContextType(raw.type);
|
|
44
|
+
const guid = cleanString(raw.guid);
|
|
45
|
+
if (type === null || guid === null) return null;
|
|
46
|
+
return {
|
|
47
|
+
type,
|
|
48
|
+
guid,
|
|
49
|
+
locationLabel: cleanString(raw.locationLabel),
|
|
50
|
+
projects: cleanStringList(raw.projects),
|
|
51
|
+
coarse: raw.coarse === true,
|
|
52
|
+
lastAnchorChangeAt: cleanEpochMs(raw.lastAnchorChangeAt)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function parseContexts(raw) {
|
|
56
|
+
if (!Array.isArray(raw)) return [];
|
|
57
|
+
const seen = /* @__PURE__ */ new Set();
|
|
58
|
+
const contexts = [];
|
|
59
|
+
for (const item of raw) {
|
|
60
|
+
const parsed = parseContext(item);
|
|
61
|
+
if (parsed === null) continue;
|
|
62
|
+
const key = `${parsed.type}:${parsed.guid}`;
|
|
63
|
+
if (seen.has(key)) continue;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
contexts.push(parsed);
|
|
66
|
+
if (contexts.length >= MAX_PRESENCE_CONTEXTS_PER_SLOT) break;
|
|
67
|
+
}
|
|
68
|
+
return contexts;
|
|
69
|
+
}
|
|
70
|
+
function parseAgentInitiator(raw) {
|
|
71
|
+
if (!isRecord(raw)) return null;
|
|
72
|
+
const initiator = {
|
|
73
|
+
userId: cleanString(raw.userId),
|
|
74
|
+
email: cleanString(raw.email),
|
|
75
|
+
name: cleanString(raw.name)
|
|
76
|
+
};
|
|
77
|
+
if (initiator.userId === null && initiator.email === null && initiator.name === null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return initiator;
|
|
81
|
+
}
|
|
82
|
+
function parseSlot(raw) {
|
|
83
|
+
if (!isRecord(raw)) return null;
|
|
84
|
+
const user = raw.user;
|
|
85
|
+
if (!isRecord(user)) return null;
|
|
86
|
+
const email = cleanString(user.email);
|
|
87
|
+
const userId = cleanString(user.userId) ?? email;
|
|
88
|
+
if (userId === null) return null;
|
|
89
|
+
return {
|
|
90
|
+
kind: user.kind === "agent" ? "agent" : "human",
|
|
91
|
+
userId,
|
|
92
|
+
authUserId: cleanString(user.authUserId),
|
|
93
|
+
name: cleanString(user.name) ?? userId,
|
|
94
|
+
email,
|
|
95
|
+
color: cleanString(user.color),
|
|
96
|
+
avatarUrl: cleanString(user.avatarUrl),
|
|
97
|
+
agentInitiator: parseAgentInitiator(user.agentInitiator),
|
|
98
|
+
contexts: parseContexts(raw.activeContexts),
|
|
99
|
+
hidden: raw.hidden === true,
|
|
100
|
+
lastIntentAt: cleanEpochMs(raw.lastIntentAt),
|
|
101
|
+
manualAway: raw.status === "away"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function mergeContexts(existing, incoming) {
|
|
105
|
+
const index = /* @__PURE__ */ new Map();
|
|
106
|
+
existing.forEach((context, position) => {
|
|
107
|
+
index.set(`${context.type}:${context.guid}`, position);
|
|
108
|
+
});
|
|
109
|
+
for (const context of incoming) {
|
|
110
|
+
const key = `${context.type}:${context.guid}`;
|
|
111
|
+
const position = index.get(key);
|
|
112
|
+
if (position === void 0) {
|
|
113
|
+
existing.push(context);
|
|
114
|
+
index.set(key, existing.length - 1);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const prior = existing[position];
|
|
118
|
+
if (prior && (context.lastAnchorChangeAt ?? 0) > (prior.lastAnchorChangeAt ?? 0)) {
|
|
119
|
+
existing[position] = context;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function maxIntent(a, b) {
|
|
124
|
+
if (a === null) return b;
|
|
125
|
+
if (b === null) return a;
|
|
126
|
+
return Math.max(a, b);
|
|
127
|
+
}
|
|
128
|
+
function toPresenceContext(context) {
|
|
129
|
+
return {
|
|
130
|
+
type: context.type,
|
|
131
|
+
guid: context.guid,
|
|
132
|
+
locationLabel: context.locationLabel,
|
|
133
|
+
projects: context.projects,
|
|
134
|
+
coarse: context.coarse
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function byName(a, b) {
|
|
138
|
+
const names = a.name.localeCompare(b.name);
|
|
139
|
+
return names !== 0 ? names : a.key.localeCompare(b.key);
|
|
140
|
+
}
|
|
141
|
+
function mergePresenceStates(states) {
|
|
142
|
+
const humans = /* @__PURE__ */ new Map();
|
|
143
|
+
const agents = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const raw of states.values()) {
|
|
145
|
+
const slot = parseSlot(raw);
|
|
146
|
+
if (slot === null) continue;
|
|
147
|
+
if (slot.kind === "agent") {
|
|
148
|
+
const existing2 = agents.get(slot.userId);
|
|
149
|
+
if (existing2) {
|
|
150
|
+
existing2.lastIntentAt = maxIntent(existing2.lastIntentAt, slot.lastIntentAt);
|
|
151
|
+
existing2.agentInitiator = existing2.agentInitiator ?? slot.agentInitiator;
|
|
152
|
+
mergeContexts(existing2.contexts, slot.contexts);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
agents.set(slot.userId, {
|
|
156
|
+
key: slot.userId,
|
|
157
|
+
userId: slot.userId,
|
|
158
|
+
name: slot.name,
|
|
159
|
+
agentInitiator: slot.agentInitiator,
|
|
160
|
+
contexts: [...slot.contexts],
|
|
161
|
+
lastIntentAt: slot.lastIntentAt
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const key = slot.authUserId ?? slot.userId;
|
|
166
|
+
const existing = humans.get(key);
|
|
167
|
+
if (existing) {
|
|
168
|
+
existing.connections += 1;
|
|
169
|
+
existing.lastIntentAt = maxIntent(existing.lastIntentAt, slot.lastIntentAt);
|
|
170
|
+
existing.hidden = existing.hidden && slot.hidden;
|
|
171
|
+
existing.manualAway = existing.manualAway || slot.manualAway;
|
|
172
|
+
existing.email = existing.email ?? slot.email;
|
|
173
|
+
existing.avatarUrl = existing.avatarUrl ?? slot.avatarUrl;
|
|
174
|
+
existing.color = existing.color ?? slot.color;
|
|
175
|
+
mergeContexts(existing.contexts, slot.contexts);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
humans.set(key, {
|
|
179
|
+
key,
|
|
180
|
+
authUserId: slot.authUserId,
|
|
181
|
+
userId: slot.userId,
|
|
182
|
+
name: slot.name,
|
|
183
|
+
email: slot.email,
|
|
184
|
+
avatarUrl: slot.avatarUrl,
|
|
185
|
+
color: slot.color,
|
|
186
|
+
contexts: [...slot.contexts],
|
|
187
|
+
lastIntentAt: slot.lastIntentAt,
|
|
188
|
+
hidden: slot.hidden,
|
|
189
|
+
manualAway: slot.manualAway,
|
|
190
|
+
connections: 1
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
humans: [...humans.values()].map((human) => ({ ...human, contexts: human.contexts.map(toPresenceContext) })).sort(byName),
|
|
195
|
+
agents: [...agents.values()].map((agent) => ({ ...agent, contexts: agent.contexts.map(toPresenceContext) })).sort(byName)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function derivePresenceState({
|
|
199
|
+
lastIntentAt,
|
|
200
|
+
hidden,
|
|
201
|
+
manualAway,
|
|
202
|
+
now
|
|
203
|
+
}) {
|
|
204
|
+
if (manualAway) return "away";
|
|
205
|
+
if (lastIntentAt === null) return "idle";
|
|
206
|
+
const elapsed = Math.max(0, now - lastIntentAt);
|
|
207
|
+
if (elapsed <= PRESENCE_ACTIVE_WINDOW_MS) return hidden ? "idle" : "active";
|
|
208
|
+
if (elapsed <= PRESENCE_IDLE_WINDOW_MS) return "idle";
|
|
209
|
+
return "away";
|
|
210
|
+
}
|
|
211
|
+
export {
|
|
212
|
+
MAX_PRESENCE_CONTEXTS_PER_SLOT,
|
|
213
|
+
MAX_PRESENCE_STRING_CHARS,
|
|
214
|
+
PRESENCE_ACTIVE_WINDOW_MS,
|
|
215
|
+
PRESENCE_CONTEXT_TYPE,
|
|
216
|
+
PRESENCE_IDLE_WINDOW_MS,
|
|
217
|
+
derivePresenceState,
|
|
218
|
+
mergePresenceStates
|
|
219
|
+
};
|
|
220
|
+
//# sourceMappingURL=presence-slots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence-slots.js","sources":["../../src/collab/presence-slots.ts"],"sourcesContent":["/**\n * Pure decoding of workspace awareness slots into typed presence entries.\n *\n * Every connected Olympus tab (and every agent run, published by the backend)\n * holds one slot in the workspace awareness room. Keryx narrows what each\n * recipient receives (a document the viewer may not open collapses into a\n * coarse `project` context, agents with nothing visible are dropped), so the\n * slots seen here are already scoped to the token holder. This module treats\n * every field as untrusted anyway: a slot is JSON a browser published, and a\n * malformed one must be dropped rather than crash the feed or merge every\n * anonymous tab into one phantom person.\n *\n * The shape mirrors Olympus's `WorkspaceAwarenessSlot`\n * (`olympus/src/store/workspaceAwarenessStore.ts`) and the bounds mirror\n * Agora's observer (`agora/agora/services/presence/awareness_slots.py`).\n * No React, no network — the merge and the intent ladder are testable directly.\n */\n\nexport const PRESENCE_CONTEXT_TYPE = {\n ASSET: 'asset',\n SESSION: 'session',\n /** Server-written only: \"somewhere in that project\", never which document. */\n PROJECT: 'project',\n} as const;\n\nexport type PresenceContextType =\n (typeof PRESENCE_CONTEXT_TYPE)[keyof typeof PRESENCE_CONTEXT_TYPE];\n\n/** Contexts kept per slot; anything past this is publisher junk. */\nexport const MAX_PRESENCE_CONTEXTS_PER_SLOT = 64;\n/** Characters kept per string field. */\nexport const MAX_PRESENCE_STRING_CHARS = 512;\n\n/**\n * Intent ladder thresholds, mirroring `agora/agora/services/presence/types.py`\n * and `olympus/src/sections/presence-monitor/presence-state.ts`. Readers\n * recompute the state from `lastIntentAt` on every tick so it decays smoothly\n * instead of stepping when a snapshot lands.\n */\nexport const PRESENCE_ACTIVE_WINDOW_MS = 60_000;\nexport const PRESENCE_IDLE_WINDOW_MS = 300_000;\n\nexport type PresenceState = 'active' | 'idle' | 'away';\n\nexport interface PresenceContext {\n type: PresenceContextType;\n /** Asset id, session thread id, or project asset id depending on `type`. */\n guid: string;\n /** Human-readable location inside the asset (\"Slide 4\", \"Sheet1!B14\"), when the tab reported one. */\n locationLabel: string | null;\n /** Server-written: projects containing this document that the viewer may also open. */\n projects: string[];\n /** Server-written on `project` contexts: the viewer may not open the underlying document. */\n coarse: boolean;\n}\n\nexport interface PresenceHuman {\n /** Identity the person's tabs merge by: the server-attested id when present, else the self-reported one. */\n key: string;\n /** Agora user id stamped by Keryx from the publishing socket's token; null on unstamped slots. */\n authUserId: string | null;\n /** Self-reported user identifier (the email, for Olympus tabs). */\n userId: string;\n name: string;\n email: string | null;\n avatarUrl: string | null;\n color: string | null;\n /** Union of open contexts across the person's tabs, deduped by type + guid. */\n contexts: PresenceContext[];\n /** Most recent self-reported input across the person's tabs (epoch ms, publisher clock); null when none reported. */\n lastIntentAt: number | null;\n /** True only when every one of the person's tabs reports itself backgrounded. */\n hidden: boolean;\n /** True when any tab carries the manual \"away\" status set in Settings. */\n manualAway: boolean;\n /** Number of awareness slots (tabs) merged into this entry. */\n connections: number;\n}\n\nexport interface PresenceAgent {\n /** The agent slot's own id (`agent:<run>`); runs never collapse by publisher. */\n key: string;\n userId: string;\n name: string;\n /** The person the run works for; what a reader shows as \"Athena for …\". */\n agentInitiator: { userId: string | null; email: string | null; name: string | null } | null;\n contexts: PresenceContext[];\n lastIntentAt: number | null;\n}\n\nexport interface PresenceRoster {\n humans: PresenceHuman[];\n agents: PresenceAgent[];\n}\n\ninterface ParsedContext extends PresenceContext {\n /** Publisher-reported ms of the last anchor change; breaks ties when tabs share a context. */\n lastAnchorChangeAt: number | null;\n}\n\ninterface ParsedSlot {\n kind: 'human' | 'agent';\n userId: string;\n authUserId: string | null;\n name: string;\n email: string | null;\n color: string | null;\n avatarUrl: string | null;\n agentInitiator: PresenceAgent['agentInitiator'];\n contexts: ParsedContext[];\n hidden: boolean;\n lastIntentAt: number | null;\n manualAway: boolean;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** Accept a non-empty string, trimmed and truncated. Anything else becomes null. */\nfunction cleanString(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (!trimmed) return null;\n return trimmed.slice(0, MAX_PRESENCE_STRING_CHARS);\n}\n\n/** Accept a plausible epoch-milliseconds number, else null. */\nfunction cleanEpochMs(value: unknown): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return null;\n return value;\n}\n\nfunction cleanContextType(value: unknown): PresenceContextType | null {\n if (\n value === PRESENCE_CONTEXT_TYPE.ASSET ||\n value === PRESENCE_CONTEXT_TYPE.SESSION ||\n value === PRESENCE_CONTEXT_TYPE.PROJECT\n ) {\n return value;\n }\n return null;\n}\n\nfunction cleanStringList(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const out: string[] = [];\n for (const item of value) {\n const cleaned = cleanString(item);\n if (cleaned === null || out.includes(cleaned)) continue;\n out.push(cleaned);\n if (out.length >= MAX_PRESENCE_CONTEXTS_PER_SLOT) break;\n }\n return out;\n}\n\nfunction parseContext(raw: unknown): ParsedContext | null {\n if (!isRecord(raw)) return null;\n const type = cleanContextType(raw.type);\n const guid = cleanString(raw.guid);\n if (type === null || guid === null) return null;\n return {\n type,\n guid,\n locationLabel: cleanString(raw.locationLabel),\n projects: cleanStringList(raw.projects),\n coarse: raw.coarse === true,\n lastAnchorChangeAt: cleanEpochMs(raw.lastAnchorChangeAt),\n };\n}\n\nfunction parseContexts(raw: unknown): ParsedContext[] {\n if (!Array.isArray(raw)) return [];\n const seen = new Set<string>();\n const contexts: ParsedContext[] = [];\n for (const item of raw) {\n const parsed = parseContext(item);\n if (parsed === null) continue;\n const key = `${parsed.type}:${parsed.guid}`;\n if (seen.has(key)) continue;\n seen.add(key);\n contexts.push(parsed);\n if (contexts.length >= MAX_PRESENCE_CONTEXTS_PER_SLOT) break;\n }\n return contexts;\n}\n\nfunction parseAgentInitiator(raw: unknown): PresenceAgent['agentInitiator'] {\n if (!isRecord(raw)) return null;\n const initiator = {\n userId: cleanString(raw.userId),\n email: cleanString(raw.email),\n name: cleanString(raw.name),\n };\n if (initiator.userId === null && initiator.email === null && initiator.name === null) {\n return null;\n }\n return initiator;\n}\n\n/**\n * Validate one slot. Returns null for anything that cannot be attributed to a\n * stable identity: a slot with neither `userId` nor `email` would otherwise\n * merge every anonymous tab in the workspace into one phantom participant.\n * A missing or unrecognised `kind` reads as human — an older client is far\n * likelier than an agent, and mislabelling a person would hide them.\n */\nfunction parseSlot(raw: unknown): ParsedSlot | null {\n if (!isRecord(raw)) return null;\n const user = raw.user;\n if (!isRecord(user)) return null;\n const email = cleanString(user.email);\n const userId = cleanString(user.userId) ?? email;\n if (userId === null) return null;\n return {\n kind: user.kind === 'agent' ? 'agent' : 'human',\n userId,\n authUserId: cleanString(user.authUserId),\n name: cleanString(user.name) ?? userId,\n email,\n color: cleanString(user.color),\n avatarUrl: cleanString(user.avatarUrl),\n agentInitiator: parseAgentInitiator(user.agentInitiator),\n contexts: parseContexts(raw.activeContexts),\n hidden: raw.hidden === true,\n lastIntentAt: cleanEpochMs(raw.lastIntentAt),\n manualAway: raw.status === 'away',\n };\n}\n\n/**\n * Fold one slot's contexts into an already-seen participant's list, keeping\n * the entry with the most recent anchor activity for a duplicate (type, guid).\n */\nfunction mergeContexts(existing: ParsedContext[], incoming: readonly ParsedContext[]): void {\n const index = new Map<string, number>();\n existing.forEach((context, position) => {\n index.set(`${context.type}:${context.guid}`, position);\n });\n for (const context of incoming) {\n const key = `${context.type}:${context.guid}`;\n const position = index.get(key);\n if (position === undefined) {\n existing.push(context);\n index.set(key, existing.length - 1);\n continue;\n }\n const prior = existing[position];\n if (prior && (context.lastAnchorChangeAt ?? 0) > (prior.lastAnchorChangeAt ?? 0)) {\n existing[position] = context;\n }\n }\n}\n\nfunction maxIntent(a: number | null, b: number | null): number | null {\n if (a === null) return b;\n if (b === null) return a;\n return Math.max(a, b);\n}\n\nfunction toPresenceContext(context: ParsedContext): PresenceContext {\n return {\n type: context.type,\n guid: context.guid,\n locationLabel: context.locationLabel,\n projects: context.projects,\n coarse: context.coarse,\n };\n}\n\ninterface HumanAccumulator extends Omit<PresenceHuman, 'contexts'> {\n contexts: ParsedContext[];\n}\n\ninterface AgentAccumulator extends Omit<PresenceAgent, 'contexts'> {\n contexts: ParsedContext[];\n}\n\nfunction byName<T extends { name: string; key: string }>(a: T, b: T): number {\n const names = a.name.localeCompare(b.name);\n return names !== 0 ? names : a.key.localeCompare(b.key);\n}\n\n/**\n * Merge the awareness room's slots into one entry per person and one per\n * agent run.\n *\n * Humans merge by the server-attested `authUserId` when Keryx stamped one\n * (a tab that merely *claims* a colleague's `userId` gets its own row, with its\n * real identity on it) and by the self-reported `userId` only for unstamped\n * slots — the same rule as Agora's observer and Olympus's store. A person is\n * as active as their most active tab, backgrounded only when every tab is,\n * and away whenever any tab carries the manual flag. Agents keep their own\n * slot id: every run is published by the same service identity, so merging\n * by publisher would collapse concurrent runs into one.\n */\nexport function mergePresenceStates(states: ReadonlyMap<number, unknown>): PresenceRoster {\n const humans = new Map<string, HumanAccumulator>();\n const agents = new Map<string, AgentAccumulator>();\n\n for (const raw of states.values()) {\n const slot = parseSlot(raw);\n if (slot === null) continue;\n\n if (slot.kind === 'agent') {\n const existing = agents.get(slot.userId);\n if (existing) {\n existing.lastIntentAt = maxIntent(existing.lastIntentAt, slot.lastIntentAt);\n existing.agentInitiator = existing.agentInitiator ?? slot.agentInitiator;\n mergeContexts(existing.contexts, slot.contexts);\n continue;\n }\n agents.set(slot.userId, {\n key: slot.userId,\n userId: slot.userId,\n name: slot.name,\n agentInitiator: slot.agentInitiator,\n contexts: [...slot.contexts],\n lastIntentAt: slot.lastIntentAt,\n });\n continue;\n }\n\n const key = slot.authUserId ?? slot.userId;\n const existing = humans.get(key);\n if (existing) {\n existing.connections += 1;\n existing.lastIntentAt = maxIntent(existing.lastIntentAt, slot.lastIntentAt);\n existing.hidden = existing.hidden && slot.hidden;\n existing.manualAway = existing.manualAway || slot.manualAway;\n // A person with tabs in two surfaces publishes identity from both;\n // prefer whichever slot actually carried the optional fields.\n existing.email = existing.email ?? slot.email;\n existing.avatarUrl = existing.avatarUrl ?? slot.avatarUrl;\n existing.color = existing.color ?? slot.color;\n mergeContexts(existing.contexts, slot.contexts);\n continue;\n }\n humans.set(key, {\n key,\n authUserId: slot.authUserId,\n userId: slot.userId,\n name: slot.name,\n email: slot.email,\n avatarUrl: slot.avatarUrl,\n color: slot.color,\n contexts: [...slot.contexts],\n lastIntentAt: slot.lastIntentAt,\n hidden: slot.hidden,\n manualAway: slot.manualAway,\n connections: 1,\n });\n }\n\n return {\n humans: [...humans.values()]\n .map((human) => ({ ...human, contexts: human.contexts.map(toPresenceContext) }))\n .sort(byName),\n agents: [...agents.values()]\n .map((agent) => ({ ...agent, contexts: agent.contexts.map(toPresenceContext) }))\n .sort(byName),\n };\n}\n\n/**\n * Where a person sits on the intent ladder right now.\n *\n * The manual \"away\" status overrides the ladder. A person who never reported\n * an input is `idle` rather than `away` — absence of evidence is not evidence\n * of leaving. Every tab backgrounded (`hidden`) caps the result below\n * `active`: whatever they did a moment ago, they are demonstrably not looking\n * at Athena now; it does not force `away`, because alt-tabbing to check\n * something is not leaving. A `lastIntentAt` ahead of `now` (a publisher whose\n * clock runs fast) counts as \"just acted\" rather than a negative elapsed.\n */\nexport function derivePresenceState({\n lastIntentAt,\n hidden,\n manualAway,\n now,\n}: {\n lastIntentAt: number | null;\n hidden: boolean;\n manualAway: boolean;\n now: number;\n}): PresenceState {\n if (manualAway) return 'away';\n if (lastIntentAt === null) return 'idle';\n const elapsed = Math.max(0, now - lastIntentAt);\n if (elapsed <= PRESENCE_ACTIVE_WINDOW_MS) return hidden ? 'idle' : 'active';\n if (elapsed <= PRESENCE_IDLE_WINDOW_MS) return 'idle';\n return 'away';\n}\n"],"names":["existing"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const REFRESH_HEADROOM_MS = 5 * 60 * 1e3;
|
|
4
|
+
const MIN_REFRESH_DELAY_MS = 30 * 1e3;
|
|
5
|
+
const MAX_CONSECUTIVE_REMINTS = 2;
|
|
6
|
+
function refreshDelayMs({ expiresAtMs, now }) {
|
|
7
|
+
return Math.max(expiresAtMs - now - REFRESH_HEADROOM_MS, MIN_REFRESH_DELAY_MS);
|
|
8
|
+
}
|
|
9
|
+
exports.MAX_CONSECUTIVE_REMINTS = MAX_CONSECUTIVE_REMINTS;
|
|
10
|
+
exports.MIN_REFRESH_DELAY_MS = MIN_REFRESH_DELAY_MS;
|
|
11
|
+
exports.REFRESH_HEADROOM_MS = REFRESH_HEADROOM_MS;
|
|
12
|
+
exports.refreshDelayMs = refreshDelayMs;
|
|
13
|
+
//# sourceMappingURL=token-refresh.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-refresh.cjs","sources":["../../src/collab/token-refresh.ts"],"sourcesContent":["/**\n * Token-refresh policy shared by every Keryx client in this package.\n *\n * Both the Generic Doc client and the workspace-presence observer mint a\n * short-lived room-bound token and must replace it before it expires. The\n * constants live here so the two clients cannot drift apart: a presence feed\n * that re-minted on a different schedule than a document would be a second\n * policy to reason about during an incident, for no benefit.\n */\n\n/** Re-mint this long before token expiry (clamped to a floor for short TTLs). */\nexport const REFRESH_HEADROOM_MS = 5 * 60 * 1000;\n\n/** Never schedule a refresh sooner than this, however short the grant. */\nexport const MIN_REFRESH_DELAY_MS = 30 * 1000;\n\n/** Consecutive 4401→mint cycles allowed without a successful connect between. */\nexport const MAX_CONSECUTIVE_REMINTS = 2;\n\n/** Milliseconds until the scheduled re-mint for a grant expiring at `expiresAtMs`. */\nexport function refreshDelayMs({ expiresAtMs, now }: { expiresAtMs: number; now: number }): number {\n return Math.max(expiresAtMs - now - REFRESH_HEADROOM_MS, MIN_REFRESH_DELAY_MS);\n}\n"],"names":[],"mappings":";;AAWO,MAAM,sBAAsB,IAAI,KAAK;AAGrC,MAAM,uBAAuB,KAAK;AAGlC,MAAM,0BAA0B;AAGhC,SAAS,eAAe,EAAE,aAAa,OAAqD;AACjG,SAAO,KAAK,IAAI,cAAc,MAAM,qBAAqB,oBAAoB;AAC/E;;;;;"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-refresh policy shared by every Keryx client in this package.
|
|
3
|
+
*
|
|
4
|
+
* Both the Generic Doc client and the workspace-presence observer mint a
|
|
5
|
+
* short-lived room-bound token and must replace it before it expires. The
|
|
6
|
+
* constants live here so the two clients cannot drift apart: a presence feed
|
|
7
|
+
* that re-minted on a different schedule than a document would be a second
|
|
8
|
+
* policy to reason about during an incident, for no benefit.
|
|
9
|
+
*/
|
|
10
|
+
/** Re-mint this long before token expiry (clamped to a floor for short TTLs). */
|
|
11
|
+
export declare const REFRESH_HEADROOM_MS: number;
|
|
12
|
+
/** Never schedule a refresh sooner than this, however short the grant. */
|
|
13
|
+
export declare const MIN_REFRESH_DELAY_MS: number;
|
|
14
|
+
/** Consecutive 4401→mint cycles allowed without a successful connect between. */
|
|
15
|
+
export declare const MAX_CONSECUTIVE_REMINTS = 2;
|
|
16
|
+
/** Milliseconds until the scheduled re-mint for a grant expiring at `expiresAtMs`. */
|
|
17
|
+
export declare function refreshDelayMs({ expiresAtMs, now }: {
|
|
18
|
+
expiresAtMs: number;
|
|
19
|
+
now: number;
|
|
20
|
+
}): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const REFRESH_HEADROOM_MS = 5 * 60 * 1e3;
|
|
2
|
+
const MIN_REFRESH_DELAY_MS = 30 * 1e3;
|
|
3
|
+
const MAX_CONSECUTIVE_REMINTS = 2;
|
|
4
|
+
function refreshDelayMs({ expiresAtMs, now }) {
|
|
5
|
+
return Math.max(expiresAtMs - now - REFRESH_HEADROOM_MS, MIN_REFRESH_DELAY_MS);
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
MAX_CONSECUTIVE_REMINTS,
|
|
9
|
+
MIN_REFRESH_DELAY_MS,
|
|
10
|
+
REFRESH_HEADROOM_MS,
|
|
11
|
+
refreshDelayMs
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=token-refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-refresh.js","sources":["../../src/collab/token-refresh.ts"],"sourcesContent":["/**\n * Token-refresh policy shared by every Keryx client in this package.\n *\n * Both the Generic Doc client and the workspace-presence observer mint a\n * short-lived room-bound token and must replace it before it expires. The\n * constants live here so the two clients cannot drift apart: a presence feed\n * that re-minted on a different schedule than a document would be a second\n * policy to reason about during an incident, for no benefit.\n */\n\n/** Re-mint this long before token expiry (clamped to a floor for short TTLs). */\nexport const REFRESH_HEADROOM_MS = 5 * 60 * 1000;\n\n/** Never schedule a refresh sooner than this, however short the grant. */\nexport const MIN_REFRESH_DELAY_MS = 30 * 1000;\n\n/** Consecutive 4401→mint cycles allowed without a successful connect between. */\nexport const MAX_CONSECUTIVE_REMINTS = 2;\n\n/** Milliseconds until the scheduled re-mint for a grant expiring at `expiresAtMs`. */\nexport function refreshDelayMs({ expiresAtMs, now }: { expiresAtMs: number; now: number }): number {\n return Math.max(expiresAtMs - now - REFRESH_HEADROOM_MS, MIN_REFRESH_DELAY_MS);\n}\n"],"names":[],"mappings":"AAWO,MAAM,sBAAsB,IAAI,KAAK;AAGrC,MAAM,uBAAuB,KAAK;AAGlC,MAAM,0BAA0B;AAGhC,SAAS,eAAe,EAAE,aAAa,OAAqD;AACjG,SAAO,KAAK,IAAI,cAAc,MAAM,qBAAqB,oBAAoB;AAC/E;"}
|
|
@@ -39,13 +39,42 @@ function pendingActionRequests(hitl) {
|
|
|
39
39
|
name: typeof request.name === "string" ? request.name : "tool",
|
|
40
40
|
args: isRecord(request.args) ? request.args : {},
|
|
41
41
|
...typeof request.tool_call_id === "string" && { toolCallId: request.tool_call_id },
|
|
42
|
-
...typeof request.interrupt_id === "string" && { interruptId: request.interrupt_id }
|
|
42
|
+
...typeof request.interrupt_id === "string" && { interruptId: request.interrupt_id },
|
|
43
|
+
...request.session_approval_allowed === true && { sessionApprovalAllowed: true }
|
|
43
44
|
}));
|
|
44
45
|
}
|
|
46
|
+
function canApproveForSession(actionRequests) {
|
|
47
|
+
return actionRequests.length > 0 && actionRequests.every((request) => request.sessionApprovalAllowed === true);
|
|
48
|
+
}
|
|
45
49
|
function readAbandonedInputRequests(state) {
|
|
46
50
|
const abandoned = statewireState.readDeepAgentStreamValues(state).abandonedInputRequests;
|
|
47
51
|
return Array.isArray(abandoned) ? abandoned : void 0;
|
|
48
52
|
}
|
|
53
|
+
function readInterruptMirror(state) {
|
|
54
|
+
return statewireState.readDeepAgentStreamValues(state).__interrupt__;
|
|
55
|
+
}
|
|
56
|
+
function interruptMirrorPayload(mirror) {
|
|
57
|
+
return isRecord(mirror) ? mirror.value : void 0;
|
|
58
|
+
}
|
|
59
|
+
function readQueuedLaneDecisions(state) {
|
|
60
|
+
const queued = statewireState.readDeepAgentStreamValues(state).queuedLaneDecisions;
|
|
61
|
+
return isRecord(queued) ? queued : void 0;
|
|
62
|
+
}
|
|
63
|
+
function laneInterruptIds(actionRequests) {
|
|
64
|
+
if (actionRequests.length === 0) return null;
|
|
65
|
+
const ids = [];
|
|
66
|
+
for (const request of actionRequests) {
|
|
67
|
+
if (request.interruptId === void 0) return null;
|
|
68
|
+
if (!ids.includes(request.interruptId)) ids.push(request.interruptId);
|
|
69
|
+
}
|
|
70
|
+
return ids;
|
|
71
|
+
}
|
|
72
|
+
function fanLaneDecision({
|
|
73
|
+
interruptIds,
|
|
74
|
+
decision
|
|
75
|
+
}) {
|
|
76
|
+
return Object.fromEntries(interruptIds.map((id) => [id, decision]));
|
|
77
|
+
}
|
|
49
78
|
function findAbandonedInterrupt(abandoned) {
|
|
50
79
|
return abandoned == null ? void 0 : abandoned.find(
|
|
51
80
|
(request) => isRecord(request) && request.type === "interrupt" && typeof request.id === "string"
|
|
@@ -56,6 +85,9 @@ const HITL_PENDING_RESUME_GRACE_MS = 15e3;
|
|
|
56
85
|
function hitlResumeApprove() {
|
|
57
86
|
return { action: "approve" };
|
|
58
87
|
}
|
|
88
|
+
function hitlResumeApproveForSession() {
|
|
89
|
+
return { action: "approve", scope: "session" };
|
|
90
|
+
}
|
|
59
91
|
function hitlResumeReject(reason = HITL_REJECT_REASON) {
|
|
60
92
|
return { action: "reject", reason };
|
|
61
93
|
}
|
|
@@ -127,15 +159,22 @@ exports.HITL_PENDING_RESUME_GRACE_MS = HITL_PENDING_RESUME_GRACE_MS;
|
|
|
127
159
|
exports.HITL_REJECT_REASON = HITL_REJECT_REASON;
|
|
128
160
|
exports.INTERRUPT_DECLINE_REASON = INTERRUPT_DECLINE_REASON;
|
|
129
161
|
exports.asHitlApproval = asHitlApproval;
|
|
162
|
+
exports.canApproveForSession = canApproveForSession;
|
|
163
|
+
exports.fanLaneDecision = fanLaneDecision;
|
|
130
164
|
exports.findAbandonedInterrupt = findAbandonedInterrupt;
|
|
131
165
|
exports.hitlResumeApprove = hitlResumeApprove;
|
|
166
|
+
exports.hitlResumeApproveForSession = hitlResumeApproveForSession;
|
|
132
167
|
exports.hitlResumeReject = hitlResumeReject;
|
|
168
|
+
exports.interruptMirrorPayload = interruptMirrorPayload;
|
|
133
169
|
exports.interruptResumeDecline = interruptResumeDecline;
|
|
134
170
|
exports.isApprovalCardInterrupt = isApprovalCardInterrupt;
|
|
135
171
|
exports.isPendingInterrupt = isPendingInterrupt;
|
|
136
172
|
exports.isSystemPauseInterrupt = isSystemPauseInterrupt;
|
|
173
|
+
exports.laneInterruptIds = laneInterruptIds;
|
|
137
174
|
exports.pendingActionRequests = pendingActionRequests;
|
|
138
175
|
exports.readAbandonedInputRequests = readAbandonedInputRequests;
|
|
139
176
|
exports.readInterruptMessage = readInterruptMessage;
|
|
177
|
+
exports.readInterruptMirror = readInterruptMirror;
|
|
178
|
+
exports.readQueuedLaneDecisions = readQueuedLaneDecisions;
|
|
140
179
|
exports.useApprovalResumeLock = useApprovalResumeLock;
|
|
141
180
|
//# sourceMappingURL=interrupts.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interrupts.cjs","sources":["../../../../../agent-runtime/src/interrupts.ts"],"sourcesContent":["/**\n * Human-in-the-loop interrupt primitives shared by every statewire chat\n * surface (Olympus, the React SDK, the Office add-in, the Chrome extension,\n * mobile).\n *\n * The agora deep-agent runtime parks a gated run on an `interrupt` input\n * request whose payload carries the Athena `hitl_approval` shape\n * (`athena_deep_agent/hitl_bridge.py`). A surface answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. This module holds the\n * payload contract (narrowing, resume values) and the shared resume-lock state\n * machine; rendering stays with each surface.\n *\n * Deliberately parameterized rather than self-wiring: these helpers never call\n * `useStatewireRuns`/`useAuiState` themselves. Each surface reads its own\n * statewire hooks (which its tests already mock) and passes the values in, so\n * the shared semantics cannot drift while per-surface wiring stays testable.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { hasClientToolInterruptSource } from './client-tools';\nimport { readDeepAgentStreamValues } from './statewire-state';\n\n/** Human-in-the-loop approval payload the agora deep-agent runtime parks a run\n * on (shaped by `athena_deep_agent/hitl_bridge.py`). */\nexport interface HitlApprovalPayload {\n source: string;\n type: string;\n message: string;\n /** Risk headline/facts/warnings, when the host includes them. */\n approval_context?: unknown;\n context?: {\n tool_name?: string;\n tool_id?: string;\n tool_call_id?: string;\n tool_args?: Record<string, unknown>;\n pending_action_count?: number;\n approval_context?: unknown;\n /**\n * Every pending gated call (name + args), in decision order. Absent on\n * older backends — fall back to the single `tool_name`/`tool_args` pair.\n * Per-dispatch payloads also carry each lane's `tool_call_id` (the lane\n * card key) and `interrupt_id` (the lane resume key).\n */\n action_requests?: {\n name?: string;\n args?: Record<string, unknown>;\n tool_call_id?: string;\n interrupt_id?: string;\n }[];\n /**\n * True when each dispatch parked on its own interrupt: decisions submit\n * per lane via `lane_decisions` and each approved lane starts immediately,\n * instead of the legacy batch where one resume must answer every lane.\n */\n per_dispatch?: boolean;\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** A pending `interrupt` input request; a present `response` key marks it answered. */\nexport function isPendingInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n return request.type === 'interrupt' && !('response' in request);\n}\n\n/**\n * Interrupt sources that were system pauses before the host stamped\n * `requires_user_action: false` on them (a background deep-research wait). Kept\n * only for the deploy skew between an older agora and a newer client; delete\n * once every deployment's raiser stamps the flag.\n */\nconst LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES: ReadonlySet<string> = new Set(['deep_research_pending']);\n\n/**\n * A park nobody answers: the agent is still working (a background research\n * wait) and the backend resumes it itself. The raiser stamps\n * `requires_user_action: false`; older raisers are recognised by source.\n */\nexport function isSystemPauseInterrupt(payload: unknown): boolean {\n if (!isRecord(payload)) return false;\n if (payload.requires_user_action === false) return true;\n return (\n typeof payload.source === 'string' && LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES.has(payload.source)\n );\n}\n\n/**\n * A pending interrupt an approval card may answer. Two kinds are excluded —\n * client-tool parks (the client-tool bridge owns those, and a generic resume\n * would corrupt the parked tool call) and system pauses (nothing to answer; the\n * backend resumes them). The source checks are deliberately loose so a skewed\n * deploy can never route one here.\n */\nexport function isApprovalCardInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (!isPendingInterrupt(request)) return false;\n const payload = (request as { payload?: unknown }).payload;\n return !hasClientToolInterruptSource(payload) && !isSystemPauseInterrupt(payload);\n}\n\n/** Narrow an interrupt payload to the Athena HITL approval shape. */\nexport function asHitlApproval(value: unknown): HitlApprovalPayload | null {\n if (!isRecord(value)) return null;\n return value.type === 'hitl' &&\n value.source === 'hitl_approval' &&\n typeof value.message === 'string'\n ? (value as unknown as HitlApprovalPayload)\n : null;\n}\n\n/** Default paused-run copy when an interrupt payload carries no message. */\nexport const DEFAULT_INTERRUPT_PAUSED_MESSAGE =\n 'The agent is paused and waiting for input.';\n\n/** Best-effort human-readable message from any interrupt payload. */\nexport function readInterruptMessage(\n value: unknown,\n fallback: string = DEFAULT_INTERRUPT_PAUSED_MESSAGE,\n): string {\n if (isRecord(value) && typeof value.message === 'string' && value.message.length > 0) {\n return value.message;\n }\n return fallback;\n}\n\nexport interface PendingActionRequest {\n name: string;\n args: Record<string, unknown>;\n /** The lane's `task` tool-call id (present on per-dispatch payloads). */\n toolCallId?: string;\n /** The lane's langgraph interrupt id — the `lane_decisions` resume key. */\n interruptId?: string;\n}\n\n/**\n * Every gated call this approval covers, in decision order. One resume answers\n * ALL of them, so every entry must stay reviewable somewhere. Falls back to\n * the single first-call fields on older backends without `action_requests`.\n */\nexport function pendingActionRequests(hitl: HitlApprovalPayload | null): PendingActionRequest[] {\n const context = hitl?.context;\n if (!context) return [];\n // The payload crosses the wire with only its top-level shape narrowed\n // (`asHitlApproval`), so the nested collection must be validated before any\n // array/property access: a malformed host value must degrade to \"no\n // reviewable entries\", never crash the approval surface.\n const requests: Record<string, unknown>[] = Array.isArray(context.action_requests)\n ? context.action_requests.filter(isRecord)\n : context.tool_name && context.tool_args\n ? [{ name: context.tool_name, args: context.tool_args }]\n : [];\n return requests.map((request) => ({\n name: typeof request.name === 'string' ? request.name : 'tool',\n args: isRecord(request.args) ? request.args : {},\n ...(typeof request.tool_call_id === 'string' && { toolCallId: request.tool_call_id }),\n ...(typeof request.interrupt_id === 'string' && { interruptId: request.interrupt_id }),\n }));\n}\n\n/** An interrupt a non-resume dispatch dropped, published by the host. */\nexport interface AbandonedInputRequest {\n id: string;\n type: string;\n}\n\n/**\n * `useStatewireState` selector body for the host-published abandoned input\n * requests. Reads through {@link readDeepAgentStreamValues} so the selector is\n * safe on the pre-attach (`undefined`) wire state, and returns the raw array\n * reference so the subscription stays shallow-stable — derive (`.find`) on the\n * result, never inside the selector.\n */\nexport function readAbandonedInputRequests(\n state: unknown,\n): readonly AbandonedInputRequest[] | undefined {\n const abandoned = readDeepAgentStreamValues(state).abandonedInputRequests;\n // Wire value: anything non-array (including a malformed host publication)\n // reads as \"none\" rather than crashing the consuming surface. Identity-\n // preserving for the valid case, so it stays selector-safe.\n return Array.isArray(abandoned)\n ? (abandoned as readonly AbandonedInputRequest[])\n : undefined;\n}\n\n/** The abandoned `interrupt` entry, if any — the \"approval was cancelled\" notice. */\nexport function findAbandonedInterrupt(\n abandoned: readonly AbandonedInputRequest[] | undefined,\n): AbandonedInputRequest | undefined {\n return abandoned?.find(\n (request) => isRecord(request) && request.type === 'interrupt' && typeof request.id === 'string',\n );\n}\n\n/** Canonical reason string carried by a user rejection. */\nexport const HITL_REJECT_REASON = 'User rejected the action.';\n\n/**\n * Default {@link UseApprovalResumeLockOptions.pendingResumeGraceMs} for every\n * approval surface: how long a clicked decision waits for its resume run to\n * start before the actions unlock again. Covers a `run/input` the host\n * rejected or a disconnected transport swallowed before any run began — the\n * one failure neither identity-change nor observed-run recovery can detect.\n */\nexport const HITL_PENDING_RESUME_GRACE_MS = 15_000;\n\n/** `run/input` resume value approving the gated call(s). */\nexport function hitlResumeApprove(): Record<string, string> {\n return { action: 'approve' };\n}\n\n/** `run/input` resume value rejecting the gated call(s). */\nexport function hitlResumeReject(reason: string = HITL_REJECT_REASON): Record<string, string> {\n return { action: 'reject', reason };\n}\n\n/** Canonical reason string carried by a user declining a non-HITL park. */\nexport const INTERRUPT_DECLINE_REASON = 'User declined the request.';\n\n/**\n * `run/input` resume value declining a non-HITL park. `{ action: 'reject' }`\n * is the one value every typed raiser treats as a clean user decline\n * (`FormCancelledError` / `ToolRejectedError`); a generic `continue` would be\n * rejected by their resume schemas and surface as a tool error the agent then\n * improvises around.\n */\nexport function interruptResumeDecline(\n reason: string = INTERRUPT_DECLINE_REASON,\n): Record<string, string> {\n return { action: 'reject', reason };\n}\n\nexport interface UseApprovalResumeLockOptions<TValue> {\n /** The pending interrupt's request id (`undefined` when nothing is parked). */\n requestId: string | undefined;\n /** The surface's thread `isRunning` read (mockable per surface). */\n isRunning: boolean;\n /**\n * Dispatch the `run/input` resume for `requestId` on the surface's transport.\n * Return the dispatch promise so a refused or lost command releases the lock\n * at once and reaches `onResumeRejected`; a `void` return leaves the\n * recovery paths below as the only release.\n */\n sendResume: (requestId: string, value: TValue) => void | Promise<unknown>;\n /**\n * The dispatch `sendResume` returned rejected — the host refused the\n * `run/input` or the transport lost it. The lock is already released when\n * this fires; the surface tells the user, since the request is back on\n * screen with nothing else saying why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n /**\n * Re-enable the actions when no resume run started within this window —\n * covers a `run/input` that was rejected or lost before any run began, so\n * neither reset path below can fire. Omit to keep the pre-existing surface\n * behavior of staying locked until a resume run is observed.\n */\n pendingResumeGraceMs?: number;\n}\n\nexport interface ApprovalResumeLock<TValue> {\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Send the resume value, locking the actions. No-op (returns `undefined`)\n * while pending or unparked. Returns the dispatch promise when `sendResume`\n * produced one, so a surface can hold its own state until the host settles.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n}\n\n/**\n * Optimistic action lock shared by every approval surface: disable the\n * decision buttons the moment one is clicked, until the request identity\n * changes (it clears after the resume, or a fresh interrupt arrives). This\n * closes the double-click → duplicate-resume window.\n *\n * Recovery paths, in order of specificity:\n * 0. The dispatch itself rejected (a `sendResume` that returned a promise) —\n * released at once, and reported through `onResumeRejected`.\n * 1. Identity change — the resume landed (or a new interrupt replaced the\n * request). Reset during render via React's \"previous value\" pattern.\n * 2. A resume run was observed starting and then settling with the request\n * still pending (failed/cancelled resume) — clear the lock so the user can\n * answer again.\n * 3. Optional grace window ({@link UseApprovalResumeLockOptions.pendingResumeGraceMs})\n * for a `run/input` that never started a run at all.\n */\nexport function useApprovalResumeLock<TValue = Record<string, string>>(\n options: UseApprovalResumeLockOptions<TValue>,\n): ApprovalResumeLock<TValue> {\n const { requestId, isRunning, sendResume, pendingResumeGraceMs, onResumeRejected } = options;\n\n const [pending, setPending] = useState(false);\n const prevRequestIdRef = useRef(requestId);\n if (prevRequestIdRef.current !== requestId) {\n prevRequestIdRef.current = requestId;\n if (pending) setPending(false);\n }\n\n // Track the resume run we kicked off (isRunning observed true→false while\n // pending) and clear `pending` when it ends with the request unresolved.\n const sawResumeRunRef = useRef(false);\n useEffect(() => {\n if (!pending) {\n sawResumeRunRef.current = false;\n return;\n }\n if (isRunning) {\n sawResumeRunRef.current = true;\n return;\n }\n if (sawResumeRunRef.current) {\n sawResumeRunRef.current = false;\n setPending(false);\n }\n }, [pending, isRunning]);\n\n // Pre-start failure recovery: re-enable after the grace window when no\n // resume run began; answering the still-parked request again is exactly\n // what the user needs to be able to do.\n useEffect(() => {\n if (!pending || pendingResumeGraceMs === undefined) return;\n const timer = setTimeout(() => {\n if (!sawResumeRunRef.current) setPending(false);\n }, pendingResumeGraceMs);\n return () => clearTimeout(timer);\n }, [pending, requestId, pendingResumeGraceMs]);\n\n const sendResumeRef = useRef(sendResume);\n sendResumeRef.current = sendResume;\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n const currentRequestIdRef = useRef(requestId);\n currentRequestIdRef.current = requestId;\n const pendingRef = useRef(pending);\n pendingRef.current = pending;\n\n const resume = useCallback(\n (value: TValue): Promise<unknown> | undefined => {\n if (requestId === undefined || pendingRef.current) return undefined;\n setPending(true);\n pendingRef.current = true;\n const dispatched: unknown = sendResumeRef.current(requestId, value);\n if (!(dispatched instanceof Promise)) return undefined;\n dispatched.then(undefined, (error: unknown) => {\n // A late refusal for a request the lock has since moved past (the\n // park was re-keyed, or a newer answer is in flight) must neither\n // release that answer's lock nor be reported as a failure of it.\n const current = currentRequestIdRef.current;\n if (current !== undefined && current !== requestId) return;\n setPending(false);\n pendingRef.current = false;\n onResumeRejectedRef.current?.(error, requestId);\n });\n return dispatched;\n },\n [requestId],\n );\n\n return { pending, resume };\n}\n"],"names":["hasClientToolInterruptSource","readDeepAgentStreamValues","useState","useRef","useEffect","useCallback"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"interrupts.cjs","sources":["../../../../../agent-runtime/src/interrupts.ts"],"sourcesContent":["/**\n * Human-in-the-loop interrupt primitives shared by every statewire chat\n * surface (Olympus, the React SDK, the Office add-in, the Chrome extension,\n * mobile).\n *\n * The agora deep-agent runtime parks a gated run on an `interrupt` input\n * request whose payload carries the Athena `hitl_approval` shape\n * (`athena_deep_agent/hitl_bridge.py`). A surface answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. This module holds the\n * payload contract (narrowing, resume values) and the shared resume-lock state\n * machine; rendering stays with each surface.\n *\n * Deliberately parameterized rather than self-wiring: these helpers never call\n * `useStatewireRuns`/`useAuiState` themselves. Each surface reads its own\n * statewire hooks (which its tests already mock) and passes the values in, so\n * the shared semantics cannot drift while per-surface wiring stays testable.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { hasClientToolInterruptSource } from './client-tools';\nimport { readDeepAgentStreamValues } from './statewire-state';\n\n/** Human-in-the-loop approval payload the agora deep-agent runtime parks a run\n * on (shaped by `athena_deep_agent/hitl_bridge.py`). */\nexport interface HitlApprovalPayload {\n source: string;\n type: string;\n message: string;\n /** Risk headline/facts/warnings, when the host includes them. */\n approval_context?: unknown;\n context?: {\n tool_name?: string;\n tool_id?: string;\n tool_call_id?: string;\n tool_args?: Record<string, unknown>;\n pending_action_count?: number;\n approval_context?: unknown;\n /**\n * Every pending gated call (name + args), in decision order. Absent on\n * older backends — fall back to the single `tool_name`/`tool_args` pair.\n * Per-dispatch payloads also carry each lane's `tool_call_id` (the lane\n * card key) and `interrupt_id` (the lane resume key).\n */\n action_requests?: {\n name?: string;\n args?: Record<string, unknown>;\n tool_call_id?: string;\n interrupt_id?: string;\n /**\n * True when \"approve all for this session\" would take effect for this\n * tool (the workspace allows overriding its approval). Absent on older\n * backends and on non-overridable tools: the option is not offered.\n */\n session_approval_allowed?: boolean;\n }[];\n /**\n * True when each dispatch parked on its own interrupt: decisions submit\n * per lane via `lane_decisions` and each approved lane starts immediately,\n * instead of the legacy batch where one resume must answer every lane.\n */\n per_dispatch?: boolean;\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** A pending `interrupt` input request; a present `response` key marks it answered. */\nexport function isPendingInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n return request.type === 'interrupt' && !('response' in request);\n}\n\n/**\n * Interrupt sources that were system pauses before the host stamped\n * `requires_user_action: false` on them (a background deep-research wait). Kept\n * only for the deploy skew between an older agora and a newer client; delete\n * once every deployment's raiser stamps the flag.\n */\nconst LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES: ReadonlySet<string> = new Set(['deep_research_pending']);\n\n/**\n * A park nobody answers: the agent is still working (a background research\n * wait) and the backend resumes it itself. The raiser stamps\n * `requires_user_action: false`; older raisers are recognised by source.\n */\nexport function isSystemPauseInterrupt(payload: unknown): boolean {\n if (!isRecord(payload)) return false;\n if (payload.requires_user_action === false) return true;\n return (\n typeof payload.source === 'string' && LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES.has(payload.source)\n );\n}\n\n/**\n * A pending interrupt an approval card may answer. Two kinds are excluded —\n * client-tool parks (the client-tool bridge owns those, and a generic resume\n * would corrupt the parked tool call) and system pauses (nothing to answer; the\n * backend resumes them). The source checks are deliberately loose so a skewed\n * deploy can never route one here.\n */\nexport function isApprovalCardInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (!isPendingInterrupt(request)) return false;\n const payload = (request as { payload?: unknown }).payload;\n return !hasClientToolInterruptSource(payload) && !isSystemPauseInterrupt(payload);\n}\n\n/** Narrow an interrupt payload to the Athena HITL approval shape. */\nexport function asHitlApproval(value: unknown): HitlApprovalPayload | null {\n if (!isRecord(value)) return null;\n return value.type === 'hitl' &&\n value.source === 'hitl_approval' &&\n typeof value.message === 'string'\n ? (value as unknown as HitlApprovalPayload)\n : null;\n}\n\n/** Default paused-run copy when an interrupt payload carries no message. */\nexport const DEFAULT_INTERRUPT_PAUSED_MESSAGE =\n 'The agent is paused and waiting for input.';\n\n/** Best-effort human-readable message from any interrupt payload. */\nexport function readInterruptMessage(\n value: unknown,\n fallback: string = DEFAULT_INTERRUPT_PAUSED_MESSAGE,\n): string {\n if (isRecord(value) && typeof value.message === 'string' && value.message.length > 0) {\n return value.message;\n }\n return fallback;\n}\n\nexport interface PendingActionRequest {\n name: string;\n args: Record<string, unknown>;\n /** The lane's `task` tool-call id (present on per-dispatch payloads). */\n toolCallId?: string;\n /** The lane's langgraph interrupt id — the `lane_decisions` resume key. */\n interruptId?: string;\n /** Present (true) when a session-wide approval would cover later calls of this tool. */\n sessionApprovalAllowed?: true;\n}\n\n/**\n * Every gated call this approval covers, in decision order. One resume answers\n * ALL of them, so every entry must stay reviewable somewhere. Falls back to\n * the single first-call fields on older backends without `action_requests`.\n */\nexport function pendingActionRequests(hitl: HitlApprovalPayload | null): PendingActionRequest[] {\n const context = hitl?.context;\n if (!context) return [];\n // The payload crosses the wire with only its top-level shape narrowed\n // (`asHitlApproval`), so the nested collection must be validated before any\n // array/property access: a malformed host value must degrade to \"no\n // reviewable entries\", never crash the approval surface.\n const requests: Record<string, unknown>[] = Array.isArray(context.action_requests)\n ? context.action_requests.filter(isRecord)\n : context.tool_name && context.tool_args\n ? [{ name: context.tool_name, args: context.tool_args }]\n : [];\n return requests.map((request) => ({\n name: typeof request.name === 'string' ? request.name : 'tool',\n args: isRecord(request.args) ? request.args : {},\n ...(typeof request.tool_call_id === 'string' && { toolCallId: request.tool_call_id }),\n ...(typeof request.interrupt_id === 'string' && { interruptId: request.interrupt_id }),\n ...(request.session_approval_allowed === true && { sessionApprovalAllowed: true }),\n }));\n}\n\n/**\n * Whether the card may offer \"approve all for this session\": every pending\n * call must be one the backend stamped as session-approvable, so a batch that\n * mixes in a tool the workspace pinned as always-prompt only offers the\n * one-time answer.\n */\nexport function canApproveForSession(actionRequests: readonly PendingActionRequest[]): boolean {\n return (\n actionRequests.length > 0 &&\n actionRequests.every((request) => request.sessionApprovalAllowed === true)\n );\n}\n\n/** An interrupt a non-resume dispatch dropped, published by the host. */\nexport interface AbandonedInputRequest {\n id: string;\n type: string;\n}\n\n/**\n * `useStatewireState` selector body for the host-published abandoned input\n * requests. Reads through {@link readDeepAgentStreamValues} so the selector is\n * safe on the pre-attach (`undefined`) wire state, and returns the raw array\n * reference so the subscription stays shallow-stable — derive (`.find`) on the\n * result, never inside the selector.\n */\nexport function readAbandonedInputRequests(\n state: unknown,\n): readonly AbandonedInputRequest[] | undefined {\n const abandoned = readDeepAgentStreamValues(state).abandonedInputRequests;\n // Wire value: anything non-array (including a malformed host publication)\n // reads as \"none\" rather than crashing the consuming surface. Identity-\n // preserving for the valid case, so it stays selector-safe.\n return Array.isArray(abandoned)\n ? (abandoned as readonly AbandonedInputRequest[])\n : undefined;\n}\n\n/**\n * `useStatewireState` selector body for the host's mirrored interrupt\n * (`__interrupt__`, the LangGraph `{value, resumable}` wrapper). The mirror\n * is published the moment a lane parks on its gate — while sibling lanes are\n * still streaming and the run manager has NOT yet published an answerable\n * input request. Raw reference, selector-safe; unwrap with\n * {@link interruptMirrorPayload}.\n */\nexport function readInterruptMirror(state: unknown): unknown {\n return readDeepAgentStreamValues(state).__interrupt__;\n}\n\n/** The payload of a mirrored interrupt wrapper, or `undefined` for anything else. */\nexport function interruptMirrorPayload(mirror: unknown): unknown {\n return isRecord(mirror) ? mirror.value : undefined;\n}\n\n/**\n * `useStatewireState` selector body for the per-dispatch lane decisions the\n * host accepted through `run/lane-input` while the run was still streaming\n * (`queuedLaneDecisions`, keyed by langgraph interrupt id). Held until the\n * run parks, then answered as a native `lane_decisions` resume; every attached\n * client reads a queued lane as decided. Raw reference, selector-safe.\n */\nexport function readQueuedLaneDecisions(\n state: unknown,\n): Readonly<Record<string, unknown>> | undefined {\n const queued = readDeepAgentStreamValues(state).queuedLaneDecisions;\n return isRecord(queued) ? queued : undefined;\n}\n\n/**\n * The distinct langgraph interrupt ids a `lane_decisions` resume answers for\n * these gated calls, in first-seen order — or `null` when any call lacks its\n * stamp (a legacy batch payload only a positional resume can answer). A\n * nested HITL request gating several calls contributes one id: the lane is\n * the interrupt, and one decision fans across its calls server-side.\n */\nexport function laneInterruptIds(actionRequests: readonly PendingActionRequest[]): string[] | null {\n if (actionRequests.length === 0) return null;\n const ids: string[] = [];\n for (const request of actionRequests) {\n if (request.interruptId === undefined) return null;\n if (!ids.includes(request.interruptId)) ids.push(request.interruptId);\n }\n return ids;\n}\n\n/** One decision for every lane — the `lane_decisions` map a run-wide approve or reject sends. */\nexport function fanLaneDecision<T>({\n interruptIds,\n decision,\n}: {\n interruptIds: readonly string[];\n decision: T;\n}): Record<string, T> {\n return Object.fromEntries(interruptIds.map((id) => [id, decision]));\n}\n\n/** The abandoned `interrupt` entry, if any — the \"approval was cancelled\" notice. */\nexport function findAbandonedInterrupt(\n abandoned: readonly AbandonedInputRequest[] | undefined,\n): AbandonedInputRequest | undefined {\n return abandoned?.find(\n (request) => isRecord(request) && request.type === 'interrupt' && typeof request.id === 'string',\n );\n}\n\n/** Canonical reason string carried by a user rejection. */\nexport const HITL_REJECT_REASON = 'User rejected the action.';\n\n/**\n * Default {@link UseApprovalResumeLockOptions.pendingResumeGraceMs} for every\n * approval surface: how long a clicked decision waits for its resume run to\n * start before the actions unlock again. Covers a `run/input` the host\n * rejected or a disconnected transport swallowed before any run began — the\n * one failure neither identity-change nor observed-run recovery can detect.\n */\nexport const HITL_PENDING_RESUME_GRACE_MS = 15_000;\n\n/** `run/input` resume value approving the gated call(s). */\nexport function hitlResumeApprove(): Record<string, string> {\n return { action: 'approve' };\n}\n\n/**\n * `run/input` resume value approving the gated call(s) AND every later call of\n * the same tool(s) on this thread. The backend records the tools from its own\n * parked interrupt (never from this payload), so the value carries no names.\n */\nexport function hitlResumeApproveForSession(): Record<string, string> {\n return { action: 'approve', scope: 'session' };\n}\n\n/** `run/input` resume value rejecting the gated call(s). */\nexport function hitlResumeReject(reason: string = HITL_REJECT_REASON): Record<string, string> {\n return { action: 'reject', reason };\n}\n\n/** Canonical reason string carried by a user declining a non-HITL park. */\nexport const INTERRUPT_DECLINE_REASON = 'User declined the request.';\n\n/**\n * `run/input` resume value declining a non-HITL park. `{ action: 'reject' }`\n * is the one value every typed raiser treats as a clean user decline\n * (`FormCancelledError` / `ToolRejectedError`); a generic `continue` would be\n * rejected by their resume schemas and surface as a tool error the agent then\n * improvises around.\n */\nexport function interruptResumeDecline(\n reason: string = INTERRUPT_DECLINE_REASON,\n): Record<string, string> {\n return { action: 'reject', reason };\n}\n\nexport interface UseApprovalResumeLockOptions<TValue> {\n /** The pending interrupt's request id (`undefined` when nothing is parked). */\n requestId: string | undefined;\n /** The surface's thread `isRunning` read (mockable per surface). */\n isRunning: boolean;\n /**\n * Dispatch the `run/input` resume for `requestId` on the surface's transport.\n * Return the dispatch promise so a refused or lost command releases the lock\n * at once and reaches `onResumeRejected`; a `void` return leaves the\n * recovery paths below as the only release.\n */\n sendResume: (requestId: string, value: TValue) => void | Promise<unknown>;\n /**\n * The dispatch `sendResume` returned rejected — the host refused the\n * `run/input` or the transport lost it. The lock is already released when\n * this fires; the surface tells the user, since the request is back on\n * screen with nothing else saying why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n /**\n * Re-enable the actions when no resume run started within this window —\n * covers a `run/input` that was rejected or lost before any run began, so\n * neither reset path below can fire. Omit to keep the pre-existing surface\n * behavior of staying locked until a resume run is observed.\n */\n pendingResumeGraceMs?: number;\n}\n\nexport interface ApprovalResumeLock<TValue> {\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Send the resume value, locking the actions. No-op (returns `undefined`)\n * while pending or unparked. Returns the dispatch promise when `sendResume`\n * produced one, so a surface can hold its own state until the host settles.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n}\n\n/**\n * Optimistic action lock shared by every approval surface: disable the\n * decision buttons the moment one is clicked, until the request identity\n * changes (it clears after the resume, or a fresh interrupt arrives). This\n * closes the double-click → duplicate-resume window.\n *\n * Recovery paths, in order of specificity:\n * 0. The dispatch itself rejected (a `sendResume` that returned a promise) —\n * released at once, and reported through `onResumeRejected`.\n * 1. Identity change — the resume landed (or a new interrupt replaced the\n * request). Reset during render via React's \"previous value\" pattern.\n * 2. A resume run was observed starting and then settling with the request\n * still pending (failed/cancelled resume) — clear the lock so the user can\n * answer again.\n * 3. Optional grace window ({@link UseApprovalResumeLockOptions.pendingResumeGraceMs})\n * for a `run/input` that never started a run at all.\n */\nexport function useApprovalResumeLock<TValue = Record<string, string>>(\n options: UseApprovalResumeLockOptions<TValue>,\n): ApprovalResumeLock<TValue> {\n const { requestId, isRunning, sendResume, pendingResumeGraceMs, onResumeRejected } = options;\n\n const [pending, setPending] = useState(false);\n const prevRequestIdRef = useRef(requestId);\n if (prevRequestIdRef.current !== requestId) {\n prevRequestIdRef.current = requestId;\n if (pending) setPending(false);\n }\n\n // Track the resume run we kicked off (isRunning observed true→false while\n // pending) and clear `pending` when it ends with the request unresolved.\n const sawResumeRunRef = useRef(false);\n useEffect(() => {\n if (!pending) {\n sawResumeRunRef.current = false;\n return;\n }\n if (isRunning) {\n sawResumeRunRef.current = true;\n return;\n }\n if (sawResumeRunRef.current) {\n sawResumeRunRef.current = false;\n setPending(false);\n }\n }, [pending, isRunning]);\n\n // Pre-start failure recovery: re-enable after the grace window when no\n // resume run began; answering the still-parked request again is exactly\n // what the user needs to be able to do.\n useEffect(() => {\n if (!pending || pendingResumeGraceMs === undefined) return;\n const timer = setTimeout(() => {\n if (!sawResumeRunRef.current) setPending(false);\n }, pendingResumeGraceMs);\n return () => clearTimeout(timer);\n }, [pending, requestId, pendingResumeGraceMs]);\n\n const sendResumeRef = useRef(sendResume);\n sendResumeRef.current = sendResume;\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n const currentRequestIdRef = useRef(requestId);\n currentRequestIdRef.current = requestId;\n const pendingRef = useRef(pending);\n pendingRef.current = pending;\n\n const resume = useCallback(\n (value: TValue): Promise<unknown> | undefined => {\n if (requestId === undefined || pendingRef.current) return undefined;\n setPending(true);\n pendingRef.current = true;\n const dispatched: unknown = sendResumeRef.current(requestId, value);\n if (!(dispatched instanceof Promise)) return undefined;\n dispatched.then(undefined, (error: unknown) => {\n // A late refusal for a request the lock has since moved past (the\n // park was re-keyed, or a newer answer is in flight) must neither\n // release that answer's lock nor be reported as a failure of it.\n const current = currentRequestIdRef.current;\n if (current !== undefined && current !== requestId) return;\n setPending(false);\n pendingRef.current = false;\n onResumeRejectedRef.current?.(error, requestId);\n });\n return dispatched;\n },\n [requestId],\n );\n\n return { pending, resume };\n}\n"],"names":["hasClientToolInterruptSource","readDeepAgentStreamValues","useState","useRef","useEffect","useCallback"],"mappings":"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"}
|
|
@@ -37,13 +37,42 @@ function pendingActionRequests(hitl) {
|
|
|
37
37
|
name: typeof request.name === "string" ? request.name : "tool",
|
|
38
38
|
args: isRecord(request.args) ? request.args : {},
|
|
39
39
|
...typeof request.tool_call_id === "string" && { toolCallId: request.tool_call_id },
|
|
40
|
-
...typeof request.interrupt_id === "string" && { interruptId: request.interrupt_id }
|
|
40
|
+
...typeof request.interrupt_id === "string" && { interruptId: request.interrupt_id },
|
|
41
|
+
...request.session_approval_allowed === true && { sessionApprovalAllowed: true }
|
|
41
42
|
}));
|
|
42
43
|
}
|
|
44
|
+
function canApproveForSession(actionRequests) {
|
|
45
|
+
return actionRequests.length > 0 && actionRequests.every((request) => request.sessionApprovalAllowed === true);
|
|
46
|
+
}
|
|
43
47
|
function readAbandonedInputRequests(state) {
|
|
44
48
|
const abandoned = readDeepAgentStreamValues(state).abandonedInputRequests;
|
|
45
49
|
return Array.isArray(abandoned) ? abandoned : void 0;
|
|
46
50
|
}
|
|
51
|
+
function readInterruptMirror(state) {
|
|
52
|
+
return readDeepAgentStreamValues(state).__interrupt__;
|
|
53
|
+
}
|
|
54
|
+
function interruptMirrorPayload(mirror) {
|
|
55
|
+
return isRecord(mirror) ? mirror.value : void 0;
|
|
56
|
+
}
|
|
57
|
+
function readQueuedLaneDecisions(state) {
|
|
58
|
+
const queued = readDeepAgentStreamValues(state).queuedLaneDecisions;
|
|
59
|
+
return isRecord(queued) ? queued : void 0;
|
|
60
|
+
}
|
|
61
|
+
function laneInterruptIds(actionRequests) {
|
|
62
|
+
if (actionRequests.length === 0) return null;
|
|
63
|
+
const ids = [];
|
|
64
|
+
for (const request of actionRequests) {
|
|
65
|
+
if (request.interruptId === void 0) return null;
|
|
66
|
+
if (!ids.includes(request.interruptId)) ids.push(request.interruptId);
|
|
67
|
+
}
|
|
68
|
+
return ids;
|
|
69
|
+
}
|
|
70
|
+
function fanLaneDecision({
|
|
71
|
+
interruptIds,
|
|
72
|
+
decision
|
|
73
|
+
}) {
|
|
74
|
+
return Object.fromEntries(interruptIds.map((id) => [id, decision]));
|
|
75
|
+
}
|
|
47
76
|
function findAbandonedInterrupt(abandoned) {
|
|
48
77
|
return abandoned == null ? void 0 : abandoned.find(
|
|
49
78
|
(request) => isRecord(request) && request.type === "interrupt" && typeof request.id === "string"
|
|
@@ -54,6 +83,9 @@ const HITL_PENDING_RESUME_GRACE_MS = 15e3;
|
|
|
54
83
|
function hitlResumeApprove() {
|
|
55
84
|
return { action: "approve" };
|
|
56
85
|
}
|
|
86
|
+
function hitlResumeApproveForSession() {
|
|
87
|
+
return { action: "approve", scope: "session" };
|
|
88
|
+
}
|
|
57
89
|
function hitlResumeReject(reason = HITL_REJECT_REASON) {
|
|
58
90
|
return { action: "reject", reason };
|
|
59
91
|
}
|
|
@@ -126,16 +158,23 @@ export {
|
|
|
126
158
|
HITL_REJECT_REASON,
|
|
127
159
|
INTERRUPT_DECLINE_REASON,
|
|
128
160
|
asHitlApproval,
|
|
161
|
+
canApproveForSession,
|
|
162
|
+
fanLaneDecision,
|
|
129
163
|
findAbandonedInterrupt,
|
|
130
164
|
hitlResumeApprove,
|
|
165
|
+
hitlResumeApproveForSession,
|
|
131
166
|
hitlResumeReject,
|
|
167
|
+
interruptMirrorPayload,
|
|
132
168
|
interruptResumeDecline,
|
|
133
169
|
isApprovalCardInterrupt,
|
|
134
170
|
isPendingInterrupt,
|
|
135
171
|
isSystemPauseInterrupt,
|
|
172
|
+
laneInterruptIds,
|
|
136
173
|
pendingActionRequests,
|
|
137
174
|
readAbandonedInputRequests,
|
|
138
175
|
readInterruptMessage,
|
|
176
|
+
readInterruptMirror,
|
|
177
|
+
readQueuedLaneDecisions,
|
|
139
178
|
useApprovalResumeLock
|
|
140
179
|
};
|
|
141
180
|
//# sourceMappingURL=interrupts.js.map
|