instar 1.3.442 → 1.3.444

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.
@@ -0,0 +1,149 @@
1
+ /**
2
+ * AmbientContributionGate — the conservative "should I speak?" gate for Slack's
3
+ * `considered`/ambient mode (Slack org integration, Pillar 1 §5.2 / §6.9).
4
+ *
5
+ * In `mention-only` mode an UNDIRECTED message (no @mention, not a DM) is dropped
6
+ * — the agent stays quiet. Ambient mode keeps that default but adds ONE narrow
7
+ * escape: for a channel that has EXPLICITLY opted into proactive contribution, an
8
+ * undirected message may run this gate, which decides whether the agent has a
9
+ * concrete, meaningful contribution worth making unprompted.
10
+ *
11
+ * ── THE INVARIANT: FAIL-TO-SILENCE ─────────────────────────────────────────────
12
+ *
13
+ * This gate can only ever make the agent QUIETER. It returns `speak: true` ONLY
14
+ * when EVERY one of these holds:
15
+ * (a) the channel is explicitly ambient-opted-in (config — default OFF everywhere),
16
+ * (b) a hard per-channel rate-limit for the rolling window is NOT exceeded,
17
+ * (c) an LLM judges it can contribute MEANINGFULLY above a conservative
18
+ * confidence threshold AND explicitly says to speak.
19
+ *
20
+ * ANY failure, uncertainty, LLM error, missing provider, unparseable verdict, or
21
+ * rate-limit breach → `speak: false`. There is NO path through this gate that
22
+ * produces an over-speak on a degraded condition. This is the deliberate MIRROR of
23
+ * the floor gate's fail-CLOSED (deny-on-error): here the safe direction is SILENCE.
24
+ * (Spec §5.2 "Fail mode: fail to silence"; §11 "the ambient/should-speak gate fails
25
+ * to silence".)
26
+ *
27
+ * ── DARK / OPT-IN ──────────────────────────────────────────────────────────────
28
+ *
29
+ * Ambient contribution is disabled for every channel by default. With NO ambient
30
+ * config, this gate is never even constructed/consulted and `_handleMessage`
31
+ * behaves byte-for-byte as today (mention-only drops undirected messages). The gate
32
+ * runs ONLY for an explicitly-opted-in channel.
33
+ *
34
+ * The LLM is reached through instar's internal `IntelligenceProvider` (the same
35
+ * injected-provider pattern as `LlmIntentClassifier` / `MessagingToneGate`), NOT a
36
+ * raw API call. A `fast` model tier is used. The call is intentionally NOT marked
37
+ * `gating: true`: a gating call would provider-SWAP on failure to keep an authority
38
+ * decision alive, but the safe failure here is to stay silent — we WANT the error to
39
+ * land in our catch and return `speak: false`, never to escalate to keep speaking.
40
+ *
41
+ * NOTE: this gate decides ONLY whether to PROCESS an undirected message. It performs
42
+ * no Slack Web API calls and sends nothing itself — `_handleMessage` does the
43
+ * processing/sending downstream exactly as it does for a directed message.
44
+ *
45
+ * Design: docs/specs/SLACK-ORG-INTEGRATION-SPEC.md §5.2, §6.9, §11.
46
+ */
47
+ import type { IntelligenceProvider } from '../core/types.js';
48
+ /** The gate's decision for a single undirected message. */
49
+ export interface AmbientDecision {
50
+ /** True ONLY when every fail-to-silence condition holds. Default false. */
51
+ speak: boolean;
52
+ /** Machine-readable reason for the decision (for logging / FP measurement). */
53
+ reason: AmbientDecisionReason;
54
+ /** Optional human-readable detail (e.g. the LLM's named contribution). */
55
+ detail?: string;
56
+ }
57
+ export type AmbientDecisionReason = 'channel-not-opted-in' | 'rate-limited' | 'no-intelligence' | 'llm-error' | 'llm-unparseable' | 'llm-declined' | 'low-confidence' | 'speak';
58
+ /** Per-channel ambient configuration. Default: ambient OFF. */
59
+ export interface AmbientChannelConfig {
60
+ /** Channels explicitly opted into proactive contribution. Default: none. */
61
+ enabledChannelIds?: string[];
62
+ /**
63
+ * Hard cap on proactive (unsolicited) messages per channel within the rolling
64
+ * window. Conservative default: 1. A bot that barges in is worse than a silent
65
+ * one, so this is deliberately tiny.
66
+ */
67
+ maxProactivePerChannel?: number;
68
+ /** Rolling rate-limit window in ms. Default: 30 minutes. */
69
+ windowMs?: number;
70
+ /**
71
+ * Conservative confidence floor for the LLM's "speak" verdict, in [0,1]. Below
72
+ * this, the gate stays silent even if the LLM said speak. Default: 0.85 (high bar).
73
+ */
74
+ minConfidence?: number;
75
+ }
76
+ export interface AmbientContributionGateDeps {
77
+ /** Per-channel ambient configuration. Absent/empty ⇒ ambient OFF everywhere. */
78
+ config?: AmbientChannelConfig;
79
+ /**
80
+ * The internal LLM provider (injected — never a direct framework import). When
81
+ * absent, the gate stays SILENT (fail-to-silence): no provider ⇒ no contribution.
82
+ */
83
+ intelligence?: IntelligenceProvider;
84
+ /** Per-call LLM timeout (ms). Default 8000. */
85
+ timeoutMs?: number;
86
+ /**
87
+ * Optional observability hook — fires on EVERY decision with the reason, so the
88
+ * FP-rate of proactive speaking can be measured before any aggressiveness change.
89
+ * Best-effort: it never affects the returned decision.
90
+ */
91
+ onDecision?: (decision: AmbientDecision, channelId: string) => void;
92
+ /** Injectable clock for deterministic tests. Defaults to Date.now. */
93
+ now?: () => number;
94
+ }
95
+ /** Context the gate needs about the channel + message. */
96
+ export interface AmbientGateInput {
97
+ /** The Slack channel id the undirected message arrived in. */
98
+ channelId: string;
99
+ /** The (cleaned) message text. */
100
+ text: string;
101
+ /** Optional channel name, purely for the LLM prompt context. */
102
+ channelName?: string;
103
+ }
104
+ export declare class AmbientContributionGate {
105
+ private readonly enabledChannels;
106
+ private readonly maxProactive;
107
+ private readonly windowMs;
108
+ private readonly minConfidence;
109
+ private readonly intelligence?;
110
+ private readonly timeoutMs;
111
+ private readonly onDecision?;
112
+ private readonly now;
113
+ /**
114
+ * Rate-limit state lives HERE — a per-channel in-memory ring of the timestamps at
115
+ * which the gate last returned speak=true. It is recorded by recordSpoke() (called
116
+ * by _handleMessage only AFTER the gate cleared and the message is actually being
117
+ * processed), so a speak=true that the caller decides not to act on does not
118
+ * consume budget. In-memory is acceptable: a restart resetting the window can only
119
+ * make the agent quieter for a moment (it never over-speaks), which is on the safe
120
+ * side of the invariant. A future durable counter could replace this without
121
+ * changing the contract.
122
+ */
123
+ private readonly proactiveTimestamps;
124
+ constructor(deps?: AmbientContributionGateDeps);
125
+ /** Is ANY channel opted into ambient contribution? Used to skip the gate entirely. */
126
+ isAnyChannelEnabled(): boolean;
127
+ /** Is this specific channel opted into ambient contribution? */
128
+ isChannelEnabled(channelId: string): boolean;
129
+ /**
130
+ * Decide whether to speak on an UNDIRECTED message in an ambient channel.
131
+ * FAIL-TO-SILENCE: returns { speak: false } on every degraded/uncertain path.
132
+ */
133
+ shouldSpeak(input: AmbientGateInput): Promise<AmbientDecision>;
134
+ /**
135
+ * Record that the agent actually spoke proactively in a channel (call AFTER the
136
+ * gate cleared and _handleMessage commits to processing). Consumes one unit of the
137
+ * rolling window budget. Idempotency is not required — each proactive turn is one
138
+ * unit.
139
+ */
140
+ recordSpoke(channelId: string): void;
141
+ /** How many proactive sends remain in the current window for a channel (for status). */
142
+ remainingBudget(channelId: string): number;
143
+ /** True iff the per-channel proactive budget is exhausted for the rolling window. */
144
+ private isRateLimited;
145
+ /** Count of proactive sends within the rolling window; prunes expired entries. */
146
+ private recentCount;
147
+ private decide;
148
+ }
149
+ //# sourceMappingURL=AmbientContributionGate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AmbientContributionGate.d.ts","sourceRoot":"","sources":["../../src/permissions/AmbientContributionGate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,+EAA+E;IAC/E,MAAM,EAAE,qBAAqB,CAAC;IAC9B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,qBAAqB,GAC7B,sBAAsB,GACtB,cAAc,GACd,iBAAiB,GACjB,WAAW,GACX,iBAAiB,GACjB,cAAc,GACd,gBAAgB,GAChB,OAAO,CAAC;AAEZ,+DAA+D;AAC/D,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;OAGG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpE,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,0DAA0D;AAC1D,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAYD,qBAAa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAuB;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAyD;IACrF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAoC;gBAE5D,IAAI,GAAE,2BAAgC;IAalD,sFAAsF;IACtF,mBAAmB,IAAI,OAAO;IAI9B,gEAAgE;IAChE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI5C;;;OAGG;IACG,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IA0DpE;;;;;OAKG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAMpC,wFAAwF;IACxF,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAK1C,qFAAqF;IACrF,OAAO,CAAC,aAAa;IAIrB,kFAAkF;IAClF,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,MAAM;CAQf"}
@@ -0,0 +1,261 @@
1
+ /**
2
+ * AmbientContributionGate — the conservative "should I speak?" gate for Slack's
3
+ * `considered`/ambient mode (Slack org integration, Pillar 1 §5.2 / §6.9).
4
+ *
5
+ * In `mention-only` mode an UNDIRECTED message (no @mention, not a DM) is dropped
6
+ * — the agent stays quiet. Ambient mode keeps that default but adds ONE narrow
7
+ * escape: for a channel that has EXPLICITLY opted into proactive contribution, an
8
+ * undirected message may run this gate, which decides whether the agent has a
9
+ * concrete, meaningful contribution worth making unprompted.
10
+ *
11
+ * ── THE INVARIANT: FAIL-TO-SILENCE ─────────────────────────────────────────────
12
+ *
13
+ * This gate can only ever make the agent QUIETER. It returns `speak: true` ONLY
14
+ * when EVERY one of these holds:
15
+ * (a) the channel is explicitly ambient-opted-in (config — default OFF everywhere),
16
+ * (b) a hard per-channel rate-limit for the rolling window is NOT exceeded,
17
+ * (c) an LLM judges it can contribute MEANINGFULLY above a conservative
18
+ * confidence threshold AND explicitly says to speak.
19
+ *
20
+ * ANY failure, uncertainty, LLM error, missing provider, unparseable verdict, or
21
+ * rate-limit breach → `speak: false`. There is NO path through this gate that
22
+ * produces an over-speak on a degraded condition. This is the deliberate MIRROR of
23
+ * the floor gate's fail-CLOSED (deny-on-error): here the safe direction is SILENCE.
24
+ * (Spec §5.2 "Fail mode: fail to silence"; §11 "the ambient/should-speak gate fails
25
+ * to silence".)
26
+ *
27
+ * ── DARK / OPT-IN ──────────────────────────────────────────────────────────────
28
+ *
29
+ * Ambient contribution is disabled for every channel by default. With NO ambient
30
+ * config, this gate is never even constructed/consulted and `_handleMessage`
31
+ * behaves byte-for-byte as today (mention-only drops undirected messages). The gate
32
+ * runs ONLY for an explicitly-opted-in channel.
33
+ *
34
+ * The LLM is reached through instar's internal `IntelligenceProvider` (the same
35
+ * injected-provider pattern as `LlmIntentClassifier` / `MessagingToneGate`), NOT a
36
+ * raw API call. A `fast` model tier is used. The call is intentionally NOT marked
37
+ * `gating: true`: a gating call would provider-SWAP on failure to keep an authority
38
+ * decision alive, but the safe failure here is to stay silent — we WANT the error to
39
+ * land in our catch and return `speak: false`, never to escalate to keep speaking.
40
+ *
41
+ * NOTE: this gate decides ONLY whether to PROCESS an undirected message. It performs
42
+ * no Slack Web API calls and sends nothing itself — `_handleMessage` does the
43
+ * processing/sending downstream exactly as it does for a directed message.
44
+ *
45
+ * Design: docs/specs/SLACK-ORG-INTEGRATION-SPEC.md §5.2, §6.9, §11.
46
+ */
47
+ const DEFAULT_MAX_PROACTIVE = 1;
48
+ const DEFAULT_WINDOW_MS = 30 * 60 * 1000; // 30 minutes
49
+ const DEFAULT_MIN_CONFIDENCE = 0.85;
50
+ export class AmbientContributionGate {
51
+ enabledChannels;
52
+ maxProactive;
53
+ windowMs;
54
+ minConfidence;
55
+ intelligence;
56
+ timeoutMs;
57
+ onDecision;
58
+ now;
59
+ /**
60
+ * Rate-limit state lives HERE — a per-channel in-memory ring of the timestamps at
61
+ * which the gate last returned speak=true. It is recorded by recordSpoke() (called
62
+ * by _handleMessage only AFTER the gate cleared and the message is actually being
63
+ * processed), so a speak=true that the caller decides not to act on does not
64
+ * consume budget. In-memory is acceptable: a restart resetting the window can only
65
+ * make the agent quieter for a moment (it never over-speaks), which is on the safe
66
+ * side of the invariant. A future durable counter could replace this without
67
+ * changing the contract.
68
+ */
69
+ proactiveTimestamps = new Map();
70
+ constructor(deps = {}) {
71
+ const cfg = deps.config ?? {};
72
+ this.enabledChannels = new Set(cfg.enabledChannelIds ?? []);
73
+ // Nullish coalescing (zero is a valid — fully-silent — cap).
74
+ this.maxProactive = cfg.maxProactivePerChannel ?? DEFAULT_MAX_PROACTIVE;
75
+ this.windowMs = cfg.windowMs ?? DEFAULT_WINDOW_MS;
76
+ this.minConfidence = cfg.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
77
+ this.intelligence = deps.intelligence;
78
+ this.timeoutMs = deps.timeoutMs ?? 8000;
79
+ this.onDecision = deps.onDecision;
80
+ this.now = deps.now ?? (() => Date.now());
81
+ }
82
+ /** Is ANY channel opted into ambient contribution? Used to skip the gate entirely. */
83
+ isAnyChannelEnabled() {
84
+ return this.enabledChannels.size > 0;
85
+ }
86
+ /** Is this specific channel opted into ambient contribution? */
87
+ isChannelEnabled(channelId) {
88
+ return this.enabledChannels.has(channelId);
89
+ }
90
+ /**
91
+ * Decide whether to speak on an UNDIRECTED message in an ambient channel.
92
+ * FAIL-TO-SILENCE: returns { speak: false } on every degraded/uncertain path.
93
+ */
94
+ async shouldSpeak(input) {
95
+ // (a) Channel opt-in. Default OFF — an un-opted channel never speaks.
96
+ if (!this.enabledChannels.has(input.channelId)) {
97
+ return this.decide(input.channelId, { speak: false, reason: 'channel-not-opted-in' });
98
+ }
99
+ // (b) Hard rate-limit. Budget exhausted in the rolling window → silent.
100
+ if (this.isRateLimited(input.channelId)) {
101
+ return this.decide(input.channelId, { speak: false, reason: 'rate-limited' });
102
+ }
103
+ // (c) LLM judgment. No provider → silent (we never speak on a heuristic guess).
104
+ if (!this.intelligence) {
105
+ return this.decide(input.channelId, { speak: false, reason: 'no-intelligence' });
106
+ }
107
+ let raw;
108
+ try {
109
+ const { systemPrompt, userPrompt } = buildAmbientPrompt(input);
110
+ raw = await this.intelligence.evaluate(`${systemPrompt}\n\n${userPrompt}`, {
111
+ model: 'fast',
112
+ temperature: 0,
113
+ maxTokens: 200,
114
+ timeoutMs: this.timeoutMs,
115
+ attribution: {
116
+ component: 'AmbientContributionGate',
117
+ category: 'gate',
118
+ // Deliberately NOT gating:true. A gating call provider-SWAPS on failure to
119
+ // keep an AUTHORITY decision alive; here the safe failure is SILENCE, so we
120
+ // let the error reach the catch below and return speak:false. Escalating to
121
+ // keep talking would be exactly the over-speak this invariant forbids.
122
+ },
123
+ });
124
+ }
125
+ catch {
126
+ // network/timeout/provider failure / circuit open → SILENCE (never escalate).
127
+ return this.decide(input.channelId, { speak: false, reason: 'llm-error' });
128
+ }
129
+ const parsed = parseAmbientVerdict(raw);
130
+ if (!parsed) {
131
+ // Unparseable LLM output is a judgment FAILURE → silence.
132
+ return this.decide(input.channelId, { speak: false, reason: 'llm-unparseable' });
133
+ }
134
+ // The LLM must explicitly say speak.
135
+ if (!parsed.speak) {
136
+ return this.decide(input.channelId, { speak: false, reason: 'llm-declined' });
137
+ }
138
+ // Conservative confidence bar. Below it (or no named contribution) → silence.
139
+ if (parsed.confidence < this.minConfidence || !parsed.contribution) {
140
+ return this.decide(input.channelId, { speak: false, reason: 'low-confidence', detail: parsed.contribution });
141
+ }
142
+ // ALL fail-to-silence conditions held → speak.
143
+ return this.decide(input.channelId, { speak: true, reason: 'speak', detail: parsed.contribution });
144
+ }
145
+ /**
146
+ * Record that the agent actually spoke proactively in a channel (call AFTER the
147
+ * gate cleared and _handleMessage commits to processing). Consumes one unit of the
148
+ * rolling window budget. Idempotency is not required — each proactive turn is one
149
+ * unit.
150
+ */
151
+ recordSpoke(channelId) {
152
+ const arr = this.proactiveTimestamps.get(channelId) ?? [];
153
+ arr.push(this.now());
154
+ this.proactiveTimestamps.set(channelId, arr);
155
+ }
156
+ /** How many proactive sends remain in the current window for a channel (for status). */
157
+ remainingBudget(channelId) {
158
+ const used = this.recentCount(channelId);
159
+ return Math.max(0, this.maxProactive - used);
160
+ }
161
+ /** True iff the per-channel proactive budget is exhausted for the rolling window. */
162
+ isRateLimited(channelId) {
163
+ return this.recentCount(channelId) >= this.maxProactive;
164
+ }
165
+ /** Count of proactive sends within the rolling window; prunes expired entries. */
166
+ recentCount(channelId) {
167
+ const arr = this.proactiveTimestamps.get(channelId);
168
+ if (!arr || arr.length === 0)
169
+ return 0;
170
+ const cutoff = this.now() - this.windowMs;
171
+ const fresh = arr.filter(ts => ts >= cutoff);
172
+ if (fresh.length !== arr.length) {
173
+ // Prune expired entries so the map can't grow unbounded.
174
+ if (fresh.length === 0)
175
+ this.proactiveTimestamps.delete(channelId);
176
+ else
177
+ this.proactiveTimestamps.set(channelId, fresh);
178
+ }
179
+ return fresh.length;
180
+ }
181
+ decide(channelId, decision) {
182
+ try {
183
+ this.onDecision?.(decision, channelId);
184
+ }
185
+ catch {
186
+ /* observability is best-effort and must never affect the verdict */
187
+ }
188
+ return decision;
189
+ }
190
+ }
191
+ /**
192
+ * Validate + clamp the LLM's raw JSON into a safe verdict. Returns null when the
193
+ * payload can't be read (→ caller fails to silence). A missing/false `speak` is a
194
+ * VALID parse (it means don't speak), so this only returns null on structural junk.
195
+ */
196
+ function parseAmbientVerdict(raw) {
197
+ if (!raw || typeof raw !== 'string')
198
+ return null;
199
+ const start = raw.indexOf('{');
200
+ const end = raw.lastIndexOf('}');
201
+ if (start === -1 || end === -1 || end < start)
202
+ return null;
203
+ let obj;
204
+ try {
205
+ obj = JSON.parse(raw.slice(start, end + 1));
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ // `speak` must be present and boolean-ish; anything else is unreadable → null,
211
+ // so the caller fails to silence rather than guessing an affirmative.
212
+ let speak;
213
+ if (typeof obj.speak === 'boolean')
214
+ speak = obj.speak;
215
+ else if (obj.speak === 'true')
216
+ speak = true;
217
+ else if (obj.speak === 'false')
218
+ speak = false;
219
+ else
220
+ return null;
221
+ // Missing/invalid confidence is treated as the floor (0) — never an optimistic 1.
222
+ let confidence = typeof obj.confidence === 'number' ? obj.confidence : Number(obj.confidence);
223
+ if (!Number.isFinite(confidence))
224
+ confidence = 0;
225
+ confidence = Math.max(0, Math.min(1, confidence));
226
+ const contribution = typeof obj.contribution === 'string' && obj.contribution.trim()
227
+ ? obj.contribution.trim().slice(0, 500)
228
+ : undefined;
229
+ return { speak, confidence, contribution };
230
+ }
231
+ /**
232
+ * Build the "should I speak?" prompt. It DEFAULTS TO SILENCE and asks the model to
233
+ * return speak:true ONLY when it can name a concrete, meaningful contribution — and
234
+ * never to interrupt a human-to-human exchange without clear value (§5.2 guardrails).
235
+ */
236
+ function buildAmbientPrompt(input) {
237
+ const systemPrompt = [
238
+ 'You are an AI agent present in a shared Slack channel. You were NOT mentioned and',
239
+ 'NO ONE addressed you. Decide whether to volunteer an UNPROMPTED contribution.',
240
+ 'Your strong default is SILENCE. A bot that barges in is worse than a silent one;',
241
+ 'the failure mode is annoyance. Speak ONLY when you can name a concrete, specific,',
242
+ 'genuinely helpful contribution that the people in the channel would welcome —',
243
+ 'never to chime in, agree, restate, or interrupt a human-to-human exchange.',
244
+ 'Return ONLY a compact JSON object, no prose, with exactly these keys:',
245
+ ' "speak": boolean — true ONLY for a clearly worthwhile unprompted contribution.',
246
+ ' "confidence": 0.0-1.0 — your confidence that speaking adds clear value AND is welcome.',
247
+ ' Be honest; when unsure, return a LOW number. Uncertainty means stay silent.',
248
+ ' "contribution": one short sentence naming the concrete contribution (omit/empty if not speaking).',
249
+ 'When in doubt, return {"speak": false, "confidence": 0.0}.',
250
+ 'Never output anything but the JSON object.',
251
+ ].join('\n');
252
+ const userPrompt = [
253
+ input.channelName ? `channel: ${input.channelName}` : 'channel: (unnamed)',
254
+ 'overheard message (you were NOT addressed):',
255
+ '"""',
256
+ (input.text || '').slice(0, 2000),
257
+ '"""',
258
+ ].join('\n');
259
+ return { systemPrompt, userPrompt };
260
+ }
261
+ //# sourceMappingURL=AmbientContributionGate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AmbientContributionGate.js","sourceRoot":"","sources":["../../src/permissions/AmbientContributionGate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * LlmIntentClassifier — the JUDGMENT-BAND intent classifier (Slack permission gate,
3
+ * Pillar 2, §6.5–6.6 / §6.6 "Layer 1 INTENT + SENSITIVITY").
4
+ *
5
+ * It implements the SAME `IntentClassifier` interface as `HeuristicIntentClassifier`
6
+ * and is interchangeable with it at the gate's `classifier` slot. The difference is
7
+ * the judgment band ABOVE the floor: an LLM call refines the sensitivity tier,
8
+ * directedness, and a non-floor action label more accurately than keyword matching.
9
+ *
10
+ * ── Two invariants this class is built around ──────────────────────────────────
11
+ *
12
+ * 1. THE FLOOR STAYS DETERMINISTIC. This class NEVER lets the LLM decide that a
13
+ * floor action ISN'T one, and it never lets the LLM downgrade a heuristic-detected
14
+ * floor candidate. It runs the deterministic `HeuristicIntentClassifier` FIRST;
15
+ * when the heuristic flags a real floor action (a `floorAction`) OR the
16
+ * ambiguous-deploy low-confidence case (action 'ambiguous', tier 4, low
17
+ * confidence → the gate routes to CLARIFY), that verdict is returned AS-IS and the
18
+ * LLM is never consulted. The LLM can only refine the NON-floor judgment band.
19
+ * (Floor enumeration + role authority remain in RolePolicy / SlackPermissionGate;
20
+ * this class only feeds them an intent and must not be the thing that misses a
21
+ * floor action.)
22
+ *
23
+ * 2. NO SILENT DEGRADATION TO A BRITTLE — OR WIDER — FALLBACK
24
+ * (docs/specs/no-silent-degradation-to-brittle-fallback.md,
25
+ * docs/signal-vs-authority.md). On ANY LLM failure (no provider, throw, timeout,
26
+ * empty/unparseable response, or a response that would WIDEN access beyond the
27
+ * deterministic floor read) we fall back to the `HeuristicIntentClassifier`
28
+ * result — which itself routes ambiguity to a tier-4 low-confidence 'ambiguous'
29
+ * (→ the gate CLARIFIES, a safe non-allow). A classification failure must NEVER
30
+ * return a confident low-tier "allow"-shaped intent on what might be sensitive.
31
+ * The fallback is therefore at-least-as-conservative as the deterministic floor.
32
+ *
33
+ * The LLM is reached through instar's internal `IntelligenceProvider` (the same
34
+ * injected-provider pattern as `TopicIntentExtractor` / `MessagingToneGate`), NOT a
35
+ * raw API call. A `fast` model tier is used, and the call is marked `gating: true`
36
+ * so the IntelligenceRouter will provider-swap on failure before the error
37
+ * propagates (and we then fail closed to the heuristic).
38
+ *
39
+ * Design: docs/specs/SLACK-ORG-INTEGRATION-SPEC.md §6.5–6.6.
40
+ */
41
+ import type { IntelligenceProvider } from '../core/types.js';
42
+ import type { RequestIntent } from './types.js';
43
+ import { type IntentClassifier } from './IntentClassifier.js';
44
+ /** Observability hook reasons for why a classification fell back to the heuristic. */
45
+ export type LlmIntentDegradeReason = 'no-intelligence' | 'error' | 'unparseable' | 'floor-deterministic';
46
+ export interface LlmIntentClassifierDeps {
47
+ /**
48
+ * The internal LLM provider (injected — never a direct framework import). When
49
+ * absent, every classification degrades to the heuristic (fail-closed).
50
+ */
51
+ intelligence?: IntelligenceProvider;
52
+ /**
53
+ * The deterministic floor + fallback classifier. Defaults to a fresh
54
+ * `HeuristicIntentClassifier`. Injectable for tests.
55
+ */
56
+ heuristic?: IntentClassifier;
57
+ /** Per-call timeout for the LLM (ms). Default 8000. */
58
+ timeoutMs?: number;
59
+ /**
60
+ * Optional observability hook — fires on every degrade-to-heuristic path with the
61
+ * reason. Best-effort: it never affects the returned verdict (which is always the
62
+ * safe heuristic result on degrade).
63
+ */
64
+ onDegrade?: (reason: LlmIntentDegradeReason) => void;
65
+ }
66
+ export declare class LlmIntentClassifier implements IntentClassifier {
67
+ private readonly intelligence?;
68
+ private readonly heuristic;
69
+ private readonly timeoutMs;
70
+ private readonly onDegrade?;
71
+ constructor(deps?: LlmIntentClassifierDeps);
72
+ classify(text: string, ctx: {
73
+ directed: boolean;
74
+ }): Promise<RequestIntent>;
75
+ private report;
76
+ }
77
+ //# sourceMappingURL=LlmIntentClassifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LlmIntentClassifier.d.ts","sourceRoot":"","sources":["../../src/permissions/LlmIntentClassifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,YAAY,CAAC;AACjE,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzF,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,OAAO,GACP,aAAa,GACb,qBAAqB,CAAC;AAE1B,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACtD;AAYD,qBAAa,mBAAoB,YAAW,gBAAgB;IAC1D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAuB;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAA2C;gBAE1D,IAAI,GAAE,uBAA4B;IAOxC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IA0DhF,OAAO,CAAC,MAAM;CAOf"}