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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +57 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/messaging/slack/SlackAdapter.d.ts +17 -0
- package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
- package/dist/messaging/slack/SlackAdapter.js +53 -5
- package/dist/messaging/slack/SlackAdapter.js.map +1 -1
- package/dist/messaging/slack/types.d.ts +17 -0
- package/dist/messaging/slack/types.d.ts.map +1 -1
- package/dist/messaging/slack/types.js.map +1 -1
- package/dist/permissions/AmbientContributionGate.d.ts +149 -0
- package/dist/permissions/AmbientContributionGate.d.ts.map +1 -0
- package/dist/permissions/AmbientContributionGate.js +261 -0
- package/dist/permissions/AmbientContributionGate.js.map +1 -0
- package/dist/permissions/LlmIntentClassifier.d.ts +77 -0
- package/dist/permissions/LlmIntentClassifier.d.ts.map +1 -0
- package/dist/permissions/LlmIntentClassifier.js +225 -0
- package/dist/permissions/LlmIntentClassifier.js.map +1 -0
- package/dist/permissions/index.d.ts +2 -0
- package/dist/permissions/index.d.ts.map +1 -1
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/index.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.443.md +25 -0
- package/upgrades/1.3.444.md +24 -0
- package/upgrades/side-effects/slack-ambient-gate.md +69 -0
- package/upgrades/side-effects/slack-llm-intent-classifier.md +67 -0
|
@@ -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"}
|