@agent-assistant/proactive 0.1.0
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 +271 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# `@agent-assistant/proactive`
|
|
2
|
+
|
|
3
|
+
**Status:** IMPLEMENTATION_READY
|
|
4
|
+
**Version:** 0.1.0 (pre-1.0, provisional)
|
|
5
|
+
**Spec:** `docs/specs/v1-proactive-spec.md`
|
|
6
|
+
**Implementation plan:** `docs/architecture/v1-proactive-implementation-plan.md`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## What This Package Does
|
|
11
|
+
|
|
12
|
+
`@agent-assistant/proactive` is the decision layer for proactive assistant behavior — any assistant action that originates without a direct user message in the current turn.
|
|
13
|
+
|
|
14
|
+
It provides:
|
|
15
|
+
|
|
16
|
+
- **ProactiveEngine** — evaluates follow-up rules and watch rules against session context, applies suppression policy, and returns structured decisions
|
|
17
|
+
- **Follow-up rule evaluation** — rules fire, suppress, or are silenced by cooldown/max-count/user-activity checks
|
|
18
|
+
- **Reminder policy** — configurable `maxReminders`, `cooldownMs`, and `suppressWhenActive` per rule
|
|
19
|
+
- **Watch rules** — long-running monitoring rules that re-schedule themselves after every evaluation; lifecycle methods for pause, resume, and cancel
|
|
20
|
+
- **SchedulerBinding interface** — thin contract to external scheduling infrastructure (relaycron); this package does not own or wrap relaycron
|
|
21
|
+
- **InMemorySchedulerBinding** — test adapter with a manual trigger helper; no external infrastructure required for tests
|
|
22
|
+
- **FollowUpEvidenceSource interface** — optional pluggable evidence injection (e.g., memory entries) for rule conditions
|
|
23
|
+
|
|
24
|
+
This package does **not** own scheduling infrastructure, session lifecycle, message delivery, or domain-specific rule logic. All of that stays in product code or other packages.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install @agent-assistant/proactive
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No `@agent-assistant/*` runtime dependencies. Only `nanoid` is required at runtime.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import {
|
|
42
|
+
createProactiveEngine,
|
|
43
|
+
InMemorySchedulerBinding,
|
|
44
|
+
} from '@agent-assistant/proactive';
|
|
45
|
+
import type { FollowUpRule } from '@agent-assistant/proactive';
|
|
46
|
+
|
|
47
|
+
// Wire a scheduler binding (InMemorySchedulerBinding for tests/dev)
|
|
48
|
+
const schedulerBinding = new InMemorySchedulerBinding();
|
|
49
|
+
|
|
50
|
+
const engine = createProactiveEngine({
|
|
51
|
+
schedulerBinding,
|
|
52
|
+
defaultReminderPolicy: {
|
|
53
|
+
maxReminders: 3,
|
|
54
|
+
cooldownMs: 3_600_000, // 1 hour
|
|
55
|
+
suppressWhenActive: true,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Register a follow-up rule (stale-thread pattern)
|
|
60
|
+
const staleThreadRule: FollowUpRule = {
|
|
61
|
+
id: 'stale-thread',
|
|
62
|
+
description: 'Follow up if the session has been silent for more than 24 hours',
|
|
63
|
+
condition: (ctx) => {
|
|
64
|
+
const inactiveMs =
|
|
65
|
+
new Date(ctx.scheduledAt).getTime() - new Date(ctx.lastActivityAt).getTime();
|
|
66
|
+
return inactiveMs > 24 * 60 * 60 * 1000;
|
|
67
|
+
},
|
|
68
|
+
policy: {
|
|
69
|
+
maxReminders: 2,
|
|
70
|
+
cooldownMs: 24 * 60 * 60 * 1000,
|
|
71
|
+
},
|
|
72
|
+
routingHint: 'cheap',
|
|
73
|
+
messageTemplate: 'Checking in — any updates on this thread?',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
engine.registerFollowUpRule(staleThreadRule);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Usage in a Capability Handler
|
|
82
|
+
|
|
83
|
+
The proactive package does not register a capability handler. Products write their own:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// In your assistant definition (product code)
|
|
87
|
+
const definition = {
|
|
88
|
+
capabilities: {
|
|
89
|
+
proactive: async (message, context) => {
|
|
90
|
+
const wakeUpContext = message.payload; // parsed from the synthetic wake-up message
|
|
91
|
+
|
|
92
|
+
// Fetch session state from your session store
|
|
93
|
+
const session = await context.sessionStore.get(wakeUpContext.sessionId);
|
|
94
|
+
|
|
95
|
+
// Evaluate follow-up rules
|
|
96
|
+
const decisions = await engine.evaluateFollowUp({
|
|
97
|
+
sessionId: wakeUpContext.sessionId,
|
|
98
|
+
scheduledAt: wakeUpContext.scheduledAt,
|
|
99
|
+
lastActivityAt: session.lastActivityAt,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (const decision of decisions) {
|
|
103
|
+
if (decision.action === 'fire') {
|
|
104
|
+
await context.runtime.emit({
|
|
105
|
+
sessionId: decision.sessionId,
|
|
106
|
+
text: decision.messageTemplate ?? 'Following up.',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Follow-Up Rules
|
|
118
|
+
|
|
119
|
+
Rules are product-supplied. The engine handles watch-rule re-scheduling via `SchedulerBinding` and reminder state. Follow-up scheduling is product-owned.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
interface FollowUpRule {
|
|
123
|
+
id: string;
|
|
124
|
+
condition(ctx: FollowUpEvaluationContext, evidence: EvidenceEntry[]): boolean | Promise<boolean>;
|
|
125
|
+
description?: string;
|
|
126
|
+
policy?: ReminderPolicy; // overrides engine default when present
|
|
127
|
+
routingHint?: 'cheap' | 'fast' | 'deep'; // defaults to 'cheap'
|
|
128
|
+
messageTemplate?: string;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Suppression order:**
|
|
133
|
+
1. User became active after the wake-up was scheduled (`lastActivityAt > scheduledAt`)
|
|
134
|
+
2. Max reminders reached for this `(sessionId, ruleId)` pair
|
|
135
|
+
3. Cooldown window not elapsed since the last reminder
|
|
136
|
+
4. Condition function returned false
|
|
137
|
+
|
|
138
|
+
The first matching suppression wins. If none match, the decision is `fire`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Watch Rules
|
|
143
|
+
|
|
144
|
+
Watch rules are long-running monitors that re-schedule themselves after every evaluation — whether or not the condition triggered.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
engine.registerWatchRule({
|
|
148
|
+
id: 'unreviewed-pr',
|
|
149
|
+
description: 'Alert if a PR has been open without review for over 2 hours',
|
|
150
|
+
condition: async (ctx) => {
|
|
151
|
+
// product fetches PR state from its own store
|
|
152
|
+
return myStore.hasUnreviewedPRsOlderThan(2 * 60 * 60 * 1000);
|
|
153
|
+
},
|
|
154
|
+
action: { type: 'notify_channel', payload: { channel: 'eng-alerts' } },
|
|
155
|
+
intervalMs: 30 * 60 * 1000, // check every 30 minutes
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Lifecycle
|
|
159
|
+
engine.pauseWatchRule('unreviewed-pr');
|
|
160
|
+
engine.resumeWatchRule('unreviewed-pr');
|
|
161
|
+
engine.cancelWatchRule('unreviewed-pr'); // permanent; re-register to restart
|
|
162
|
+
|
|
163
|
+
// List statuses
|
|
164
|
+
const statuses = engine.listWatchRules();
|
|
165
|
+
// [{ rule, status: 'active' | 'paused' | 'cancelled', lastEvaluatedAt, nextWakeUpBindingId }]
|
|
166
|
+
|
|
167
|
+
// Evaluate (called from your proactive capability handler after each wake-up)
|
|
168
|
+
const triggers = await engine.evaluateWatchRules({
|
|
169
|
+
ruleId: wakeUpContext.ruleId,
|
|
170
|
+
scheduledAt: wakeUpContext.scheduledAt,
|
|
171
|
+
metadata: wakeUpContext.metadata,
|
|
172
|
+
});
|
|
173
|
+
for (const trigger of triggers) {
|
|
174
|
+
await handleWatchAction(trigger.action, trigger.context);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Scheduler Binding
|
|
181
|
+
|
|
182
|
+
The `SchedulerBinding` interface decouples the engine from scheduling infrastructure.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
interface SchedulerBinding {
|
|
186
|
+
requestWakeUp(at: Date, context: WakeUpContext): Promise<string>; // returns bindingId
|
|
187
|
+
cancelWakeUp(bindingId: string): Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**For tests and local development:** use the built-in `InMemorySchedulerBinding`:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
const binding = new InMemorySchedulerBinding();
|
|
195
|
+
|
|
196
|
+
// Inspect pending wake-ups
|
|
197
|
+
console.log(binding.pendingWakeUps); // Map<bindingId, { at, context }>
|
|
198
|
+
|
|
199
|
+
// Manually fire a wake-up in tests
|
|
200
|
+
const context = await binding.trigger(bindingId);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**For production:** implement `SchedulerBinding` against your relaycron client. The proactive package does not implement or ship a relaycron integration — that is a product/foundation concern.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Evidence Sources
|
|
208
|
+
|
|
209
|
+
Optional. Wire an evidence source to give rule conditions access to recent memory entries or other context:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const evidenceSource = {
|
|
213
|
+
getRecentEntries: (sessionId, opts) =>
|
|
214
|
+
memoryStore.retrieve({ scope: { kind: 'session', sessionId }, limit: opts?.limit }),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const engine = createProactiveEngine({ schedulerBinding, evidenceSource });
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
If configured, the engine calls `getRecentEntries` once per `evaluateFollowUp` call (shared across all rules in that call). If `context.evidence` is pre-fetched and provided, the source is not called at all.
|
|
221
|
+
|
|
222
|
+
If not configured, condition functions receive an empty evidence array.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Reminder State Management
|
|
227
|
+
|
|
228
|
+
Reminder state is in-memory and keyed by `(sessionId, ruleId)`.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// Clear state for a specific rule in a session (e.g., when user resolves the thread)
|
|
232
|
+
engine.resetReminderState('session-abc', 'stale-thread');
|
|
233
|
+
|
|
234
|
+
// Clear all reminder state for a session (e.g., on session close)
|
|
235
|
+
engine.resetReminderState('session-abc');
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## What Stays Outside This Package
|
|
241
|
+
|
|
242
|
+
| Concern | Where it lives |
|
|
243
|
+
|---|---|
|
|
244
|
+
| Scheduling infrastructure (timers, cron, dispatch) | Relay foundation (relaycron) |
|
|
245
|
+
| Domain-specific rule definitions | Product repos |
|
|
246
|
+
| Product-specific timing thresholds | Product configuration |
|
|
247
|
+
| Memory persistence | `@agent-assistant/memory` (via `FollowUpEvidenceSource`) |
|
|
248
|
+
| Session lifecycle | `@agent-assistant/sessions` |
|
|
249
|
+
| Outbound message delivery | `@agent-assistant/surfaces` + Relay runtime |
|
|
250
|
+
| Cross-agent coordination of proactive actions | `@agent-assistant/coordination` (v1.2) |
|
|
251
|
+
| Proactive action rate limiting / budgets | `@agent-assistant/policy` (v2) |
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Package Structure
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
packages/proactive/
|
|
259
|
+
package.json — nanoid runtime dep only
|
|
260
|
+
tsconfig.json
|
|
261
|
+
src/
|
|
262
|
+
types.ts — all exported types, interfaces, error classes
|
|
263
|
+
proactive.ts — createProactiveEngine factory and all engine logic
|
|
264
|
+
index.ts — public re-exports
|
|
265
|
+
proactive.test.ts — 45 tests
|
|
266
|
+
README.md
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
PROACTIVE_PACKAGE_DIRECTION_READY
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-assistant/proactive",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Proactive decision engine for Agent Assistant SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"nanoid": "^5.0.7"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"vitest": "^3.2.4"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/AgentWorkforce/agent-assistant"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|