dashclaw 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -103
- package/dashclaw.js +476 -467
- package/package.json +4 -4
package/dashclaw.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* Full-featured agent toolkit for the DashClaw platform.
|
|
4
4
|
* Zero-dependency ESM SDK — requires Node 18+ (native fetch).
|
|
5
5
|
*
|
|
6
|
-
* 60+ methods across 13+ categories:
|
|
7
|
-
* - Action Recording (7)
|
|
8
|
-
* - Loops & Assumptions (7)
|
|
9
|
-
* - Signals (1)
|
|
10
|
-
* - Dashboard Data (9)
|
|
6
|
+
* 60+ methods across 13+ categories:
|
|
7
|
+
* - Action Recording (7)
|
|
8
|
+
* - Loops & Assumptions (7)
|
|
9
|
+
* - Signals (1)
|
|
10
|
+
* - Dashboard Data (9)
|
|
11
11
|
* - Session Handoffs (3)
|
|
12
12
|
* - Context Manager (7)
|
|
13
|
-
* - Automation Snippets (
|
|
13
|
+
* - Automation Snippets (5)
|
|
14
14
|
* - User Preferences (6)
|
|
15
15
|
* - Daily Digest (1)
|
|
16
16
|
* - Security Scanning (2)
|
|
@@ -19,63 +19,63 @@
|
|
|
19
19
|
* - Bulk Sync (1)
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
class DashClaw {
|
|
22
|
+
class DashClaw {
|
|
23
23
|
/**
|
|
24
24
|
* @param {Object} options
|
|
25
|
-
* @param {string} options.baseUrl - DashClaw base URL (e.g. "https://your-app.vercel.app")
|
|
25
|
+
* @param {string} options.baseUrl - DashClaw base URL (e.g. "http://localhost:3000" or "https://your-app.vercel.app")
|
|
26
26
|
* @param {string} options.apiKey - API key for authentication (determines which org's data you access)
|
|
27
27
|
* @param {string} options.agentId - Unique identifier for this agent
|
|
28
28
|
* @param {string} [options.agentName] - Human-readable agent name
|
|
29
29
|
* @param {string} [options.swarmId] - Swarm/group identifier if part of a multi-agent system
|
|
30
|
-
* @param {string} [options.guardMode='off'] - Auto guard check before createAction: 'off' | 'warn' | 'enforce'
|
|
31
|
-
* @param {Function} [options.guardCallback] - Called with guard decision object when guardMode is active
|
|
32
|
-
* @param {string} [options.autoRecommend='off'] - Recommendation mode: 'off' | 'warn' | 'enforce'
|
|
33
|
-
* @param {number} [options.recommendationConfidenceMin=70] - Minimum recommendation confidence to auto-apply in enforce mode
|
|
34
|
-
* @param {Function} [options.recommendationCallback] - Called with recommendation adaptation details when autoRecommend is active
|
|
35
|
-
* @param {string} [options.hitlMode='off'] - How to handle pending approvals: 'off' (return immediately) | 'wait' (block and poll)
|
|
36
|
-
* @param {CryptoKey} [options.privateKey] - Web Crypto API Private Key for signing actions
|
|
37
|
-
*/
|
|
38
|
-
constructor({
|
|
39
|
-
baseUrl,
|
|
40
|
-
apiKey,
|
|
41
|
-
agentId,
|
|
42
|
-
agentName,
|
|
43
|
-
swarmId,
|
|
44
|
-
guardMode,
|
|
45
|
-
guardCallback,
|
|
46
|
-
autoRecommend,
|
|
47
|
-
recommendationConfidenceMin,
|
|
48
|
-
recommendationCallback,
|
|
49
|
-
hitlMode,
|
|
50
|
-
privateKey
|
|
51
|
-
}) {
|
|
30
|
+
* @param {string} [options.guardMode='off'] - Auto guard check before createAction: 'off' | 'warn' | 'enforce'
|
|
31
|
+
* @param {Function} [options.guardCallback] - Called with guard decision object when guardMode is active
|
|
32
|
+
* @param {string} [options.autoRecommend='off'] - Recommendation mode: 'off' | 'warn' | 'enforce'
|
|
33
|
+
* @param {number} [options.recommendationConfidenceMin=70] - Minimum recommendation confidence to auto-apply in enforce mode
|
|
34
|
+
* @param {Function} [options.recommendationCallback] - Called with recommendation adaptation details when autoRecommend is active
|
|
35
|
+
* @param {string} [options.hitlMode='off'] - How to handle pending approvals: 'off' (return immediately) | 'wait' (block and poll)
|
|
36
|
+
* @param {CryptoKey} [options.privateKey] - Web Crypto API Private Key for signing actions
|
|
37
|
+
*/
|
|
38
|
+
constructor({
|
|
39
|
+
baseUrl,
|
|
40
|
+
apiKey,
|
|
41
|
+
agentId,
|
|
42
|
+
agentName,
|
|
43
|
+
swarmId,
|
|
44
|
+
guardMode,
|
|
45
|
+
guardCallback,
|
|
46
|
+
autoRecommend,
|
|
47
|
+
recommendationConfidenceMin,
|
|
48
|
+
recommendationCallback,
|
|
49
|
+
hitlMode,
|
|
50
|
+
privateKey
|
|
51
|
+
}) {
|
|
52
52
|
if (!baseUrl) throw new Error('baseUrl is required');
|
|
53
53
|
if (!apiKey) throw new Error('apiKey is required');
|
|
54
54
|
if (!agentId) throw new Error('agentId is required');
|
|
55
55
|
|
|
56
56
|
const validModes = ['off', 'warn', 'enforce'];
|
|
57
|
-
if (guardMode && !validModes.includes(guardMode)) {
|
|
58
|
-
throw new Error(`guardMode must be one of: ${validModes.join(', ')}`);
|
|
59
|
-
}
|
|
60
|
-
if (autoRecommend && !validModes.includes(autoRecommend)) {
|
|
61
|
-
throw new Error(`autoRecommend must be one of: ${validModes.join(', ')}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
65
|
-
this.apiKey = apiKey;
|
|
57
|
+
if (guardMode && !validModes.includes(guardMode)) {
|
|
58
|
+
throw new Error(`guardMode must be one of: ${validModes.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
if (autoRecommend && !validModes.includes(autoRecommend)) {
|
|
61
|
+
throw new Error(`autoRecommend must be one of: ${validModes.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
65
|
+
this.apiKey = apiKey;
|
|
66
66
|
this.agentId = agentId;
|
|
67
67
|
this.agentName = agentName || null;
|
|
68
|
-
this.swarmId = swarmId || null;
|
|
69
|
-
this.guardMode = guardMode || 'off';
|
|
70
|
-
this.guardCallback = guardCallback || null;
|
|
71
|
-
this.autoRecommend = autoRecommend || 'off';
|
|
72
|
-
const parsedConfidenceMin = Number(recommendationConfidenceMin);
|
|
73
|
-
this.recommendationConfidenceMin = Number.isFinite(parsedConfidenceMin)
|
|
74
|
-
? Math.max(0, Math.min(parsedConfidenceMin, 100))
|
|
75
|
-
: 70;
|
|
76
|
-
this.recommendationCallback = recommendationCallback || null;
|
|
77
|
-
this.hitlMode = hitlMode || 'off';
|
|
78
|
-
this.privateKey = privateKey || null;
|
|
68
|
+
this.swarmId = swarmId || null;
|
|
69
|
+
this.guardMode = guardMode || 'off';
|
|
70
|
+
this.guardCallback = guardCallback || null;
|
|
71
|
+
this.autoRecommend = autoRecommend || 'off';
|
|
72
|
+
const parsedConfidenceMin = Number(recommendationConfidenceMin);
|
|
73
|
+
this.recommendationConfidenceMin = Number.isFinite(parsedConfidenceMin)
|
|
74
|
+
? Math.max(0, Math.min(parsedConfidenceMin, 100))
|
|
75
|
+
: 70;
|
|
76
|
+
this.recommendationCallback = recommendationCallback || null;
|
|
77
|
+
this.hitlMode = hitlMode || 'off';
|
|
78
|
+
this.privateKey = privateKey || null;
|
|
79
79
|
|
|
80
80
|
// Auto-import JWK if passed as plain object
|
|
81
81
|
if (this.privateKey && typeof this.privateKey === 'object' && this.privateKey.kty) {
|
|
@@ -83,7 +83,7 @@ class DashClaw {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
async _importJwk(jwk) {
|
|
86
|
+
async _importJwk(jwk) {
|
|
87
87
|
try {
|
|
88
88
|
const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
|
|
89
89
|
this.privateKey = await cryptoSubtle.importKey(
|
|
@@ -100,7 +100,7 @@ class DashClaw {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
async _request(path, method, body) {
|
|
103
|
+
async _request(path, method, body) {
|
|
104
104
|
const url = `${this.baseUrl}${path}`;
|
|
105
105
|
const headers = {
|
|
106
106
|
'Content-Type': 'application/json',
|
|
@@ -123,74 +123,74 @@ class DashClaw {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
return data;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Create an agent pairing request (returns a link the user can click to approve).
|
|
130
|
-
*
|
|
131
|
-
* @param {Object} options
|
|
132
|
-
* @param {string} options.publicKeyPem - PEM public key (SPKI) to register for this agent.
|
|
133
|
-
* @param {string} [options.algorithm='RSASSA-PKCS1-v1_5']
|
|
134
|
-
* @param {string} [options.agentName]
|
|
135
|
-
* @returns {Promise<{pairing: Object, pairing_url: string}>}
|
|
136
|
-
*/
|
|
137
|
-
async createPairing({ publicKeyPem, algorithm = 'RSASSA-PKCS1-v1_5', agentName } = {}) {
|
|
138
|
-
if (!publicKeyPem) throw new Error('publicKeyPem is required');
|
|
139
|
-
return this._request('/api/pairings', 'POST', {
|
|
140
|
-
agent_id: this.agentId,
|
|
141
|
-
agent_name: agentName || this.agentName,
|
|
142
|
-
public_key: publicKeyPem,
|
|
143
|
-
algorithm,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async _derivePublicKeyPemFromPrivateJwk(privateJwk) {
|
|
148
|
-
// Node-only helper (works in the typical agent runtime).
|
|
149
|
-
const { createPrivateKey, createPublicKey } = await import('node:crypto');
|
|
150
|
-
const priv = createPrivateKey({ key: privateJwk, format: 'jwk' });
|
|
151
|
-
const pub = createPublicKey(priv);
|
|
152
|
-
return pub.export({ type: 'spki', format: 'pem' });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Convenience: derive public PEM from a private JWK and create a pairing request.
|
|
157
|
-
* @param {Object} privateJwk
|
|
158
|
-
* @param {Object} [options]
|
|
159
|
-
* @param {string} [options.agentName]
|
|
160
|
-
*/
|
|
161
|
-
async createPairingFromPrivateJwk(privateJwk, { agentName } = {}) {
|
|
162
|
-
if (!privateJwk) throw new Error('privateJwk is required');
|
|
163
|
-
const publicKeyPem = await this._derivePublicKeyPemFromPrivateJwk(privateJwk);
|
|
164
|
-
return this.createPairing({ publicKeyPem, agentName });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Poll a pairing until it is approved/expired.
|
|
169
|
-
* @param {string} pairingId
|
|
170
|
-
* @param {Object} [options]
|
|
171
|
-
* @param {number} [options.timeout=300000] - Max wait time (5 min)
|
|
172
|
-
* @param {number} [options.interval=2000] - Poll interval
|
|
173
|
-
* @returns {Promise<Object>} pairing object
|
|
174
|
-
*/
|
|
175
|
-
async waitForPairing(pairingId, { timeout = 300000, interval = 2000 } = {}) {
|
|
176
|
-
const start = Date.now();
|
|
177
|
-
while (Date.now() - start < timeout) {
|
|
178
|
-
const res = await this._request(`/api/pairings/${encodeURIComponent(pairingId)}`, 'GET');
|
|
179
|
-
const pairing = res.pairing;
|
|
180
|
-
if (!pairing) throw new Error('Pairing response missing pairing');
|
|
181
|
-
if (pairing.status === 'approved') return pairing;
|
|
182
|
-
if (pairing.status === 'expired') throw new Error('Pairing expired');
|
|
183
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
184
|
-
}
|
|
185
|
-
throw new Error('Timed out waiting for pairing approval');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Internal: check guard policies before action creation.
|
|
190
|
-
* Only active when guardMode is 'warn' or 'enforce'.
|
|
191
|
-
* @param {Object} actionDef - Action definition from createAction()
|
|
192
|
-
*/
|
|
193
|
-
async _guardCheck(actionDef) {
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create an agent pairing request (returns a link the user can click to approve).
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} options
|
|
132
|
+
* @param {string} options.publicKeyPem - PEM public key (SPKI) to register for this agent.
|
|
133
|
+
* @param {string} [options.algorithm='RSASSA-PKCS1-v1_5']
|
|
134
|
+
* @param {string} [options.agentName]
|
|
135
|
+
* @returns {Promise<{pairing: Object, pairing_url: string}>}
|
|
136
|
+
*/
|
|
137
|
+
async createPairing({ publicKeyPem, algorithm = 'RSASSA-PKCS1-v1_5', agentName } = {}) {
|
|
138
|
+
if (!publicKeyPem) throw new Error('publicKeyPem is required');
|
|
139
|
+
return this._request('/api/pairings', 'POST', {
|
|
140
|
+
agent_id: this.agentId,
|
|
141
|
+
agent_name: agentName || this.agentName,
|
|
142
|
+
public_key: publicKeyPem,
|
|
143
|
+
algorithm,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async _derivePublicKeyPemFromPrivateJwk(privateJwk) {
|
|
148
|
+
// Node-only helper (works in the typical agent runtime).
|
|
149
|
+
const { createPrivateKey, createPublicKey } = await import('node:crypto');
|
|
150
|
+
const priv = createPrivateKey({ key: privateJwk, format: 'jwk' });
|
|
151
|
+
const pub = createPublicKey(priv);
|
|
152
|
+
return pub.export({ type: 'spki', format: 'pem' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Convenience: derive public PEM from a private JWK and create a pairing request.
|
|
157
|
+
* @param {Object} privateJwk
|
|
158
|
+
* @param {Object} [options]
|
|
159
|
+
* @param {string} [options.agentName]
|
|
160
|
+
*/
|
|
161
|
+
async createPairingFromPrivateJwk(privateJwk, { agentName } = {}) {
|
|
162
|
+
if (!privateJwk) throw new Error('privateJwk is required');
|
|
163
|
+
const publicKeyPem = await this._derivePublicKeyPemFromPrivateJwk(privateJwk);
|
|
164
|
+
return this.createPairing({ publicKeyPem, agentName });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Poll a pairing until it is approved/expired.
|
|
169
|
+
* @param {string} pairingId
|
|
170
|
+
* @param {Object} [options]
|
|
171
|
+
* @param {number} [options.timeout=300000] - Max wait time (5 min)
|
|
172
|
+
* @param {number} [options.interval=2000] - Poll interval
|
|
173
|
+
* @returns {Promise<Object>} pairing object
|
|
174
|
+
*/
|
|
175
|
+
async waitForPairing(pairingId, { timeout = 300000, interval = 2000 } = {}) {
|
|
176
|
+
const start = Date.now();
|
|
177
|
+
while (Date.now() - start < timeout) {
|
|
178
|
+
const res = await this._request(`/api/pairings/${encodeURIComponent(pairingId)}`, 'GET');
|
|
179
|
+
const pairing = res.pairing;
|
|
180
|
+
if (!pairing) throw new Error('Pairing response missing pairing');
|
|
181
|
+
if (pairing.status === 'approved') return pairing;
|
|
182
|
+
if (pairing.status === 'expired') throw new Error('Pairing expired');
|
|
183
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
184
|
+
}
|
|
185
|
+
throw new Error('Timed out waiting for pairing approval');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Internal: check guard policies before action creation.
|
|
190
|
+
* Only active when guardMode is 'warn' or 'enforce'.
|
|
191
|
+
* @param {Object} actionDef - Action definition from createAction()
|
|
192
|
+
*/
|
|
193
|
+
async _guardCheck(actionDef) {
|
|
194
194
|
if (this.guardMode === 'off') return;
|
|
195
195
|
|
|
196
196
|
const context = {
|
|
@@ -223,172 +223,172 @@ class DashClaw {
|
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
if (this.guardMode === 'enforce' && isBlocked) {
|
|
227
|
-
throw new GuardBlockedError(decision);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
_canonicalJsonStringify(value) {
|
|
232
|
-
const canonicalize = (v) => {
|
|
233
|
-
if (v === null) return 'null';
|
|
234
|
-
|
|
235
|
-
const t = typeof v;
|
|
236
|
-
if (t === 'string' || t === 'number' || t === 'boolean') return JSON.stringify(v);
|
|
237
|
-
|
|
238
|
-
if (t === 'undefined') return 'null';
|
|
239
|
-
|
|
240
|
-
if (Array.isArray(v)) {
|
|
241
|
-
return `[${v.map((x) => (typeof x === 'undefined' ? 'null' : canonicalize(x))).join(',')}]`;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (t === 'object') {
|
|
245
|
-
const keys = Object.keys(v)
|
|
246
|
-
.filter((k) => typeof v[k] !== 'undefined')
|
|
247
|
-
.sort();
|
|
248
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return 'null';
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
return canonicalize(value);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
_toBase64(bytes) {
|
|
258
|
-
if (typeof btoa === 'function') {
|
|
259
|
-
return btoa(String.fromCharCode(...bytes));
|
|
260
|
-
}
|
|
261
|
-
return Buffer.from(bytes).toString('base64');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
_isRestrictiveDecision(decision) {
|
|
265
|
-
return decision?.decision === 'block' || decision?.decision === 'require_approval';
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
_buildGuardContext(actionDef) {
|
|
269
|
-
return {
|
|
270
|
-
action_type: actionDef.action_type,
|
|
271
|
-
risk_score: actionDef.risk_score,
|
|
272
|
-
systems_touched: actionDef.systems_touched,
|
|
273
|
-
reversible: actionDef.reversible,
|
|
274
|
-
declared_goal: actionDef.declared_goal,
|
|
275
|
-
agent_id: this.agentId,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async _reportRecommendationEvent(event) {
|
|
280
|
-
try {
|
|
281
|
-
await this._request('/api/learning/recommendations/events', 'POST', {
|
|
282
|
-
...event,
|
|
283
|
-
agent_id: event.agent_id || this.agentId,
|
|
284
|
-
});
|
|
285
|
-
} catch {
|
|
286
|
-
// Telemetry should never break action execution
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async _autoRecommend(actionDef) {
|
|
291
|
-
if (this.autoRecommend === 'off' || !actionDef?.action_type) {
|
|
292
|
-
return { action: actionDef, recommendation: null, adapted_fields: [] };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
let result;
|
|
296
|
-
try {
|
|
297
|
-
result = await this.recommendAction(actionDef);
|
|
298
|
-
} catch (err) {
|
|
299
|
-
console.warn(`[DashClaw] Recommendation fetch failed (proceeding): ${err.message}`);
|
|
300
|
-
return { action: actionDef, recommendation: null, adapted_fields: [] };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (this.recommendationCallback) {
|
|
304
|
-
try { this.recommendationCallback(result); } catch { /* ignore callback errors */ }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const recommendation = result.recommendation || null;
|
|
308
|
-
if (!recommendation) return result;
|
|
309
|
-
|
|
310
|
-
const confidence = Number(recommendation.confidence || 0);
|
|
311
|
-
if (confidence < this.recommendationConfidenceMin) {
|
|
312
|
-
const override_reason = `confidence_below_threshold:${confidence}<${this.recommendationConfidenceMin}`;
|
|
313
|
-
await this._reportRecommendationEvent({
|
|
314
|
-
recommendation_id: recommendation.id,
|
|
315
|
-
event_type: 'overridden',
|
|
316
|
-
details: { action_type: actionDef.action_type, reason: override_reason },
|
|
317
|
-
});
|
|
318
|
-
return {
|
|
319
|
-
...result,
|
|
320
|
-
action: {
|
|
321
|
-
...actionDef,
|
|
322
|
-
recommendation_id: recommendation.id,
|
|
323
|
-
recommendation_applied: false,
|
|
324
|
-
recommendation_override_reason: override_reason,
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
let guardDecision = null;
|
|
330
|
-
try {
|
|
331
|
-
guardDecision = await this.guard(this._buildGuardContext(result.action || actionDef));
|
|
332
|
-
} catch (err) {
|
|
333
|
-
console.warn(`[DashClaw] Recommendation guard probe failed: ${err.message}`);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (this._isRestrictiveDecision(guardDecision)) {
|
|
337
|
-
const override_reason = `guard_restrictive:${guardDecision.decision}`;
|
|
338
|
-
await this._reportRecommendationEvent({
|
|
339
|
-
recommendation_id: recommendation.id,
|
|
340
|
-
event_type: 'overridden',
|
|
341
|
-
details: { action_type: actionDef.action_type, reason: override_reason },
|
|
342
|
-
});
|
|
343
|
-
return {
|
|
344
|
-
...result,
|
|
345
|
-
action: {
|
|
346
|
-
...actionDef,
|
|
347
|
-
recommendation_id: recommendation.id,
|
|
348
|
-
recommendation_applied: false,
|
|
349
|
-
recommendation_override_reason: override_reason,
|
|
350
|
-
},
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (this.autoRecommend === 'warn') {
|
|
355
|
-
const override_reason = 'warn_mode_no_autoadapt';
|
|
356
|
-
await this._reportRecommendationEvent({
|
|
357
|
-
recommendation_id: recommendation.id,
|
|
358
|
-
event_type: 'overridden',
|
|
359
|
-
details: { action_type: actionDef.action_type, reason: override_reason },
|
|
360
|
-
});
|
|
361
|
-
return {
|
|
362
|
-
...result,
|
|
363
|
-
action: {
|
|
364
|
-
...actionDef,
|
|
365
|
-
recommendation_id: recommendation.id,
|
|
366
|
-
recommendation_applied: false,
|
|
367
|
-
recommendation_override_reason: override_reason,
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
await this._reportRecommendationEvent({
|
|
373
|
-
recommendation_id: recommendation.id,
|
|
374
|
-
event_type: 'applied',
|
|
375
|
-
details: {
|
|
376
|
-
action_type: actionDef.action_type,
|
|
377
|
-
adapted_fields: result.adapted_fields || [],
|
|
378
|
-
confidence,
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
...result,
|
|
384
|
-
action: {
|
|
385
|
-
...(result.action || actionDef),
|
|
386
|
-
recommendation_id: recommendation.id,
|
|
387
|
-
recommendation_applied: true,
|
|
388
|
-
recommendation_override_reason: null,
|
|
389
|
-
},
|
|
390
|
-
};
|
|
391
|
-
}
|
|
226
|
+
if (this.guardMode === 'enforce' && isBlocked) {
|
|
227
|
+
throw new GuardBlockedError(decision);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_canonicalJsonStringify(value) {
|
|
232
|
+
const canonicalize = (v) => {
|
|
233
|
+
if (v === null) return 'null';
|
|
234
|
+
|
|
235
|
+
const t = typeof v;
|
|
236
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return JSON.stringify(v);
|
|
237
|
+
|
|
238
|
+
if (t === 'undefined') return 'null';
|
|
239
|
+
|
|
240
|
+
if (Array.isArray(v)) {
|
|
241
|
+
return `[${v.map((x) => (typeof x === 'undefined' ? 'null' : canonicalize(x))).join(',')}]`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (t === 'object') {
|
|
245
|
+
const keys = Object.keys(v)
|
|
246
|
+
.filter((k) => typeof v[k] !== 'undefined')
|
|
247
|
+
.sort();
|
|
248
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(v[k])}`).join(',')}}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return 'null';
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
return canonicalize(value);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_toBase64(bytes) {
|
|
258
|
+
if (typeof btoa === 'function') {
|
|
259
|
+
return btoa(String.fromCharCode(...bytes));
|
|
260
|
+
}
|
|
261
|
+
return Buffer.from(bytes).toString('base64');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_isRestrictiveDecision(decision) {
|
|
265
|
+
return decision?.decision === 'block' || decision?.decision === 'require_approval';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_buildGuardContext(actionDef) {
|
|
269
|
+
return {
|
|
270
|
+
action_type: actionDef.action_type,
|
|
271
|
+
risk_score: actionDef.risk_score,
|
|
272
|
+
systems_touched: actionDef.systems_touched,
|
|
273
|
+
reversible: actionDef.reversible,
|
|
274
|
+
declared_goal: actionDef.declared_goal,
|
|
275
|
+
agent_id: this.agentId,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async _reportRecommendationEvent(event) {
|
|
280
|
+
try {
|
|
281
|
+
await this._request('/api/learning/recommendations/events', 'POST', {
|
|
282
|
+
...event,
|
|
283
|
+
agent_id: event.agent_id || this.agentId,
|
|
284
|
+
});
|
|
285
|
+
} catch {
|
|
286
|
+
// Telemetry should never break action execution
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async _autoRecommend(actionDef) {
|
|
291
|
+
if (this.autoRecommend === 'off' || !actionDef?.action_type) {
|
|
292
|
+
return { action: actionDef, recommendation: null, adapted_fields: [] };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let result;
|
|
296
|
+
try {
|
|
297
|
+
result = await this.recommendAction(actionDef);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.warn(`[DashClaw] Recommendation fetch failed (proceeding): ${err.message}`);
|
|
300
|
+
return { action: actionDef, recommendation: null, adapted_fields: [] };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (this.recommendationCallback) {
|
|
304
|
+
try { this.recommendationCallback(result); } catch { /* ignore callback errors */ }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const recommendation = result.recommendation || null;
|
|
308
|
+
if (!recommendation) return result;
|
|
309
|
+
|
|
310
|
+
const confidence = Number(recommendation.confidence || 0);
|
|
311
|
+
if (confidence < this.recommendationConfidenceMin) {
|
|
312
|
+
const override_reason = `confidence_below_threshold:${confidence}<${this.recommendationConfidenceMin}`;
|
|
313
|
+
await this._reportRecommendationEvent({
|
|
314
|
+
recommendation_id: recommendation.id,
|
|
315
|
+
event_type: 'overridden',
|
|
316
|
+
details: { action_type: actionDef.action_type, reason: override_reason },
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
...result,
|
|
320
|
+
action: {
|
|
321
|
+
...actionDef,
|
|
322
|
+
recommendation_id: recommendation.id,
|
|
323
|
+
recommendation_applied: false,
|
|
324
|
+
recommendation_override_reason: override_reason,
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let guardDecision = null;
|
|
330
|
+
try {
|
|
331
|
+
guardDecision = await this.guard(this._buildGuardContext(result.action || actionDef));
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.warn(`[DashClaw] Recommendation guard probe failed: ${err.message}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (this._isRestrictiveDecision(guardDecision)) {
|
|
337
|
+
const override_reason = `guard_restrictive:${guardDecision.decision}`;
|
|
338
|
+
await this._reportRecommendationEvent({
|
|
339
|
+
recommendation_id: recommendation.id,
|
|
340
|
+
event_type: 'overridden',
|
|
341
|
+
details: { action_type: actionDef.action_type, reason: override_reason },
|
|
342
|
+
});
|
|
343
|
+
return {
|
|
344
|
+
...result,
|
|
345
|
+
action: {
|
|
346
|
+
...actionDef,
|
|
347
|
+
recommendation_id: recommendation.id,
|
|
348
|
+
recommendation_applied: false,
|
|
349
|
+
recommendation_override_reason: override_reason,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (this.autoRecommend === 'warn') {
|
|
355
|
+
const override_reason = 'warn_mode_no_autoadapt';
|
|
356
|
+
await this._reportRecommendationEvent({
|
|
357
|
+
recommendation_id: recommendation.id,
|
|
358
|
+
event_type: 'overridden',
|
|
359
|
+
details: { action_type: actionDef.action_type, reason: override_reason },
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
...result,
|
|
363
|
+
action: {
|
|
364
|
+
...actionDef,
|
|
365
|
+
recommendation_id: recommendation.id,
|
|
366
|
+
recommendation_applied: false,
|
|
367
|
+
recommendation_override_reason: override_reason,
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
await this._reportRecommendationEvent({
|
|
373
|
+
recommendation_id: recommendation.id,
|
|
374
|
+
event_type: 'applied',
|
|
375
|
+
details: {
|
|
376
|
+
action_type: actionDef.action_type,
|
|
377
|
+
adapted_fields: result.adapted_fields || [],
|
|
378
|
+
confidence,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
...result,
|
|
384
|
+
action: {
|
|
385
|
+
...(result.action || actionDef),
|
|
386
|
+
recommendation_id: recommendation.id,
|
|
387
|
+
recommendation_applied: true,
|
|
388
|
+
recommendation_override_reason: null,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
392
|
|
|
393
393
|
// ══════════════════════════════════════════════
|
|
394
394
|
// Category 1: Action Recording (6 methods)
|
|
@@ -411,39 +411,39 @@ class DashClaw {
|
|
|
411
411
|
* @param {number} [action.confidence=50] - Confidence level 0-100
|
|
412
412
|
* @returns {Promise<{action: Object, action_id: string}>}
|
|
413
413
|
*/
|
|
414
|
-
async createAction(action) {
|
|
415
|
-
const recommendationResult = await this._autoRecommend(action);
|
|
416
|
-
const finalAction = recommendationResult.action || action;
|
|
417
|
-
|
|
418
|
-
await this._guardCheck(finalAction);
|
|
419
|
-
if (this._pendingKeyImport) await this._pendingKeyImport;
|
|
420
|
-
|
|
421
|
-
const payload = {
|
|
422
|
-
agent_id: this.agentId,
|
|
423
|
-
agent_name: this.agentName,
|
|
424
|
-
swarm_id: this.swarmId,
|
|
425
|
-
...finalAction
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
let signature = null;
|
|
429
|
-
if (this.privateKey) {
|
|
430
|
-
try {
|
|
431
|
-
const encoder = new TextEncoder();
|
|
432
|
-
const data = encoder.encode(this._canonicalJsonStringify(payload));
|
|
433
|
-
// Use global crypto or fallback to node:crypto
|
|
434
|
-
const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
|
|
435
|
-
|
|
436
|
-
const sigBuffer = await cryptoSubtle.sign(
|
|
437
|
-
{ name: "RSASSA-PKCS1-v1_5" },
|
|
438
|
-
this.privateKey,
|
|
439
|
-
data
|
|
440
|
-
);
|
|
441
|
-
// Base64 encode signature
|
|
442
|
-
signature = this._toBase64(new Uint8Array(sigBuffer));
|
|
443
|
-
} catch (err) {
|
|
444
|
-
throw new Error(`Failed to sign action: ${err.message}`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
414
|
+
async createAction(action) {
|
|
415
|
+
const recommendationResult = await this._autoRecommend(action);
|
|
416
|
+
const finalAction = recommendationResult.action || action;
|
|
417
|
+
|
|
418
|
+
await this._guardCheck(finalAction);
|
|
419
|
+
if (this._pendingKeyImport) await this._pendingKeyImport;
|
|
420
|
+
|
|
421
|
+
const payload = {
|
|
422
|
+
agent_id: this.agentId,
|
|
423
|
+
agent_name: this.agentName,
|
|
424
|
+
swarm_id: this.swarmId,
|
|
425
|
+
...finalAction
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
let signature = null;
|
|
429
|
+
if (this.privateKey) {
|
|
430
|
+
try {
|
|
431
|
+
const encoder = new TextEncoder();
|
|
432
|
+
const data = encoder.encode(this._canonicalJsonStringify(payload));
|
|
433
|
+
// Use global crypto or fallback to node:crypto
|
|
434
|
+
const cryptoSubtle = globalThis.crypto?.subtle || (await import('node:crypto')).webcrypto.subtle;
|
|
435
|
+
|
|
436
|
+
const sigBuffer = await cryptoSubtle.sign(
|
|
437
|
+
{ name: "RSASSA-PKCS1-v1_5" },
|
|
438
|
+
this.privateKey,
|
|
439
|
+
data
|
|
440
|
+
);
|
|
441
|
+
// Base64 encode signature
|
|
442
|
+
signature = this._toBase64(new Uint8Array(sigBuffer));
|
|
443
|
+
} catch (err) {
|
|
444
|
+
throw new Error(`Failed to sign action: ${err.message}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
447
|
|
|
448
448
|
const res = await this._request('/api/actions', 'POST', {
|
|
449
449
|
...payload,
|
|
@@ -725,153 +725,153 @@ class DashClaw {
|
|
|
725
725
|
* @param {number} [entry.confidence] - Confidence level 0-100
|
|
726
726
|
* @returns {Promise<{decision: Object}>}
|
|
727
727
|
*/
|
|
728
|
-
async recordDecision(entry) {
|
|
729
|
-
return this._request('/api/learning', 'POST', {
|
|
730
|
-
...entry,
|
|
731
|
-
agent_id: this.agentId
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Get adaptive learning recommendations derived from prior episodes.
|
|
737
|
-
* @param {Object} [filters]
|
|
738
|
-
* @param {string} [filters.action_type] - Filter by action type
|
|
739
|
-
* @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
|
|
740
|
-
* @param {boolean} [filters.include_inactive] - Include disabled recommendations (admin/service only)
|
|
741
|
-
* @param {boolean} [filters.track_events=true] - Record recommendation fetched telemetry
|
|
742
|
-
* @param {boolean} [filters.include_metrics] - Include computed metrics in the response payload
|
|
743
|
-
* @param {number} [filters.limit=50] - Max recommendations to return
|
|
744
|
-
* @param {number} [filters.lookback_days=30] - Lookback days used when include_metrics=true
|
|
745
|
-
* @returns {Promise<{recommendations: Object[], metrics?: Object, total: number, lastUpdated: string}>}
|
|
746
|
-
*/
|
|
747
|
-
async getRecommendations(filters = {}) {
|
|
748
|
-
const params = new URLSearchParams({
|
|
749
|
-
agent_id: filters.agent_id || this.agentId,
|
|
750
|
-
});
|
|
751
|
-
if (filters.action_type) params.set('action_type', filters.action_type);
|
|
752
|
-
if (filters.limit) params.set('limit', String(filters.limit));
|
|
753
|
-
if (filters.include_inactive) params.set('include_inactive', 'true');
|
|
754
|
-
if (filters.track_events !== false) params.set('track_events', 'true');
|
|
755
|
-
if (filters.include_metrics) params.set('include_metrics', 'true');
|
|
756
|
-
if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
|
|
757
|
-
return this._request(`/api/learning/recommendations?${params}`, 'GET');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Get recommendation effectiveness metrics and telemetry aggregates.
|
|
762
|
-
* @param {Object} [filters]
|
|
763
|
-
* @param {string} [filters.action_type] - Filter by action type
|
|
764
|
-
* @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
|
|
765
|
-
* @param {number} [filters.lookback_days=30] - Lookback window for episodes/events
|
|
766
|
-
* @param {number} [filters.limit=100] - Max recommendations considered
|
|
767
|
-
* @param {boolean} [filters.include_inactive] - Include inactive recommendations (admin/service only)
|
|
768
|
-
* @returns {Promise<{metrics: Object[], summary: Object, lookback_days: number, lastUpdated: string}>}
|
|
769
|
-
*/
|
|
770
|
-
async getRecommendationMetrics(filters = {}) {
|
|
771
|
-
const params = new URLSearchParams({
|
|
772
|
-
agent_id: filters.agent_id || this.agentId,
|
|
773
|
-
});
|
|
774
|
-
if (filters.action_type) params.set('action_type', filters.action_type);
|
|
775
|
-
if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
|
|
776
|
-
if (filters.limit) params.set('limit', String(filters.limit));
|
|
777
|
-
if (filters.include_inactive) params.set('include_inactive', 'true');
|
|
778
|
-
return this._request(`/api/learning/recommendations/metrics?${params}`, 'GET');
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* Record recommendation telemetry events (single event or batch).
|
|
783
|
-
* @param {Object|Object[]} events
|
|
784
|
-
* @returns {Promise<{created: Object[], created_count: number}>}
|
|
785
|
-
*/
|
|
786
|
-
async recordRecommendationEvents(events) {
|
|
787
|
-
if (Array.isArray(events)) {
|
|
788
|
-
return this._request('/api/learning/recommendations/events', 'POST', { events });
|
|
789
|
-
}
|
|
790
|
-
return this._request('/api/learning/recommendations/events', 'POST', events || {});
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/**
|
|
794
|
-
* Enable or disable a recommendation.
|
|
795
|
-
* @param {string} recommendationId - Recommendation ID
|
|
796
|
-
* @param {boolean} active - Desired active state
|
|
797
|
-
* @returns {Promise<{recommendation: Object}>}
|
|
798
|
-
*/
|
|
799
|
-
async setRecommendationActive(recommendationId, active) {
|
|
800
|
-
return this._request(`/api/learning/recommendations/${recommendationId}`, 'PATCH', { active: !!active });
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* Rebuild recommendations from scored learning episodes.
|
|
805
|
-
* @param {Object} [options]
|
|
806
|
-
* @param {string} [options.action_type] - Scope rebuild to one action type
|
|
807
|
-
* @param {string} [options.agent_id] - Override agent_id (defaults to SDK agent)
|
|
808
|
-
* @param {number} [options.lookback_days=30] - Days of episode history to analyze
|
|
809
|
-
* @param {number} [options.min_samples=5] - Minimum episodes required per recommendation
|
|
810
|
-
* @param {number} [options.episode_limit=5000] - Episode scan cap
|
|
811
|
-
* @param {string} [options.action_id] - Optionally score this action before rebuild
|
|
812
|
-
* @returns {Promise<{recommendations: Object[], total: number, episodes_scanned: number}>}
|
|
813
|
-
*/
|
|
814
|
-
async rebuildRecommendations(options = {}) {
|
|
815
|
-
return this._request('/api/learning/recommendations', 'POST', {
|
|
816
|
-
agent_id: options.agent_id || this.agentId,
|
|
817
|
-
action_type: options.action_type,
|
|
818
|
-
lookback_days: options.lookback_days,
|
|
819
|
-
min_samples: options.min_samples,
|
|
820
|
-
episode_limit: options.episode_limit,
|
|
821
|
-
action_id: options.action_id,
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* Apply top recommendation hints to an action definition (non-destructive).
|
|
827
|
-
* @param {Object} action - Action payload compatible with createAction()
|
|
828
|
-
* @returns {Promise<{action: Object, recommendation: Object|null, adapted_fields: string[]}>}
|
|
829
|
-
*/
|
|
830
|
-
async recommendAction(action) {
|
|
831
|
-
if (!action?.action_type) {
|
|
832
|
-
return { action, recommendation: null, adapted_fields: [] };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
const response = await this.getRecommendations({ action_type: action.action_type, limit: 1 });
|
|
836
|
-
const recommendation = response.recommendations?.[0] || null;
|
|
837
|
-
if (!recommendation) {
|
|
838
|
-
return { action, recommendation: null, adapted_fields: [] };
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const adapted = { ...action };
|
|
842
|
-
const adaptedFields = [];
|
|
843
|
-
const hints = recommendation.hints || {};
|
|
844
|
-
|
|
845
|
-
if (
|
|
846
|
-
typeof hints.preferred_risk_cap === 'number' &&
|
|
847
|
-
(adapted.risk_score === undefined || adapted.risk_score > hints.preferred_risk_cap)
|
|
848
|
-
) {
|
|
849
|
-
adapted.risk_score = hints.preferred_risk_cap;
|
|
850
|
-
adaptedFields.push('risk_score');
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (hints.prefer_reversible === true && adapted.reversible === undefined) {
|
|
854
|
-
adapted.reversible = true;
|
|
855
|
-
adaptedFields.push('reversible');
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
if (
|
|
859
|
-
typeof hints.confidence_floor === 'number' &&
|
|
860
|
-
(adapted.confidence === undefined || adapted.confidence < hints.confidence_floor)
|
|
861
|
-
) {
|
|
862
|
-
adapted.confidence = hints.confidence_floor;
|
|
863
|
-
adaptedFields.push('confidence');
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
return {
|
|
867
|
-
action: adapted,
|
|
868
|
-
recommendation,
|
|
869
|
-
adapted_fields: adaptedFields,
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* Create a goal.
|
|
728
|
+
async recordDecision(entry) {
|
|
729
|
+
return this._request('/api/learning', 'POST', {
|
|
730
|
+
...entry,
|
|
731
|
+
agent_id: this.agentId
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Get adaptive learning recommendations derived from prior episodes.
|
|
737
|
+
* @param {Object} [filters]
|
|
738
|
+
* @param {string} [filters.action_type] - Filter by action type
|
|
739
|
+
* @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
|
|
740
|
+
* @param {boolean} [filters.include_inactive] - Include disabled recommendations (admin/service only)
|
|
741
|
+
* @param {boolean} [filters.track_events=true] - Record recommendation fetched telemetry
|
|
742
|
+
* @param {boolean} [filters.include_metrics] - Include computed metrics in the response payload
|
|
743
|
+
* @param {number} [filters.limit=50] - Max recommendations to return
|
|
744
|
+
* @param {number} [filters.lookback_days=30] - Lookback days used when include_metrics=true
|
|
745
|
+
* @returns {Promise<{recommendations: Object[], metrics?: Object, total: number, lastUpdated: string}>}
|
|
746
|
+
*/
|
|
747
|
+
async getRecommendations(filters = {}) {
|
|
748
|
+
const params = new URLSearchParams({
|
|
749
|
+
agent_id: filters.agent_id || this.agentId,
|
|
750
|
+
});
|
|
751
|
+
if (filters.action_type) params.set('action_type', filters.action_type);
|
|
752
|
+
if (filters.limit) params.set('limit', String(filters.limit));
|
|
753
|
+
if (filters.include_inactive) params.set('include_inactive', 'true');
|
|
754
|
+
if (filters.track_events !== false) params.set('track_events', 'true');
|
|
755
|
+
if (filters.include_metrics) params.set('include_metrics', 'true');
|
|
756
|
+
if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
|
|
757
|
+
return this._request(`/api/learning/recommendations?${params}`, 'GET');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Get recommendation effectiveness metrics and telemetry aggregates.
|
|
762
|
+
* @param {Object} [filters]
|
|
763
|
+
* @param {string} [filters.action_type] - Filter by action type
|
|
764
|
+
* @param {string} [filters.agent_id] - Override agent_id (defaults to SDK agent)
|
|
765
|
+
* @param {number} [filters.lookback_days=30] - Lookback window for episodes/events
|
|
766
|
+
* @param {number} [filters.limit=100] - Max recommendations considered
|
|
767
|
+
* @param {boolean} [filters.include_inactive] - Include inactive recommendations (admin/service only)
|
|
768
|
+
* @returns {Promise<{metrics: Object[], summary: Object, lookback_days: number, lastUpdated: string}>}
|
|
769
|
+
*/
|
|
770
|
+
async getRecommendationMetrics(filters = {}) {
|
|
771
|
+
const params = new URLSearchParams({
|
|
772
|
+
agent_id: filters.agent_id || this.agentId,
|
|
773
|
+
});
|
|
774
|
+
if (filters.action_type) params.set('action_type', filters.action_type);
|
|
775
|
+
if (filters.lookback_days) params.set('lookback_days', String(filters.lookback_days));
|
|
776
|
+
if (filters.limit) params.set('limit', String(filters.limit));
|
|
777
|
+
if (filters.include_inactive) params.set('include_inactive', 'true');
|
|
778
|
+
return this._request(`/api/learning/recommendations/metrics?${params}`, 'GET');
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Record recommendation telemetry events (single event or batch).
|
|
783
|
+
* @param {Object|Object[]} events
|
|
784
|
+
* @returns {Promise<{created: Object[], created_count: number}>}
|
|
785
|
+
*/
|
|
786
|
+
async recordRecommendationEvents(events) {
|
|
787
|
+
if (Array.isArray(events)) {
|
|
788
|
+
return this._request('/api/learning/recommendations/events', 'POST', { events });
|
|
789
|
+
}
|
|
790
|
+
return this._request('/api/learning/recommendations/events', 'POST', events || {});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Enable or disable a recommendation.
|
|
795
|
+
* @param {string} recommendationId - Recommendation ID
|
|
796
|
+
* @param {boolean} active - Desired active state
|
|
797
|
+
* @returns {Promise<{recommendation: Object}>}
|
|
798
|
+
*/
|
|
799
|
+
async setRecommendationActive(recommendationId, active) {
|
|
800
|
+
return this._request(`/api/learning/recommendations/${recommendationId}`, 'PATCH', { active: !!active });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Rebuild recommendations from scored learning episodes.
|
|
805
|
+
* @param {Object} [options]
|
|
806
|
+
* @param {string} [options.action_type] - Scope rebuild to one action type
|
|
807
|
+
* @param {string} [options.agent_id] - Override agent_id (defaults to SDK agent)
|
|
808
|
+
* @param {number} [options.lookback_days=30] - Days of episode history to analyze
|
|
809
|
+
* @param {number} [options.min_samples=5] - Minimum episodes required per recommendation
|
|
810
|
+
* @param {number} [options.episode_limit=5000] - Episode scan cap
|
|
811
|
+
* @param {string} [options.action_id] - Optionally score this action before rebuild
|
|
812
|
+
* @returns {Promise<{recommendations: Object[], total: number, episodes_scanned: number}>}
|
|
813
|
+
*/
|
|
814
|
+
async rebuildRecommendations(options = {}) {
|
|
815
|
+
return this._request('/api/learning/recommendations', 'POST', {
|
|
816
|
+
agent_id: options.agent_id || this.agentId,
|
|
817
|
+
action_type: options.action_type,
|
|
818
|
+
lookback_days: options.lookback_days,
|
|
819
|
+
min_samples: options.min_samples,
|
|
820
|
+
episode_limit: options.episode_limit,
|
|
821
|
+
action_id: options.action_id,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Apply top recommendation hints to an action definition (non-destructive).
|
|
827
|
+
* @param {Object} action - Action payload compatible with createAction()
|
|
828
|
+
* @returns {Promise<{action: Object, recommendation: Object|null, adapted_fields: string[]}>}
|
|
829
|
+
*/
|
|
830
|
+
async recommendAction(action) {
|
|
831
|
+
if (!action?.action_type) {
|
|
832
|
+
return { action, recommendation: null, adapted_fields: [] };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const response = await this.getRecommendations({ action_type: action.action_type, limit: 1 });
|
|
836
|
+
const recommendation = response.recommendations?.[0] || null;
|
|
837
|
+
if (!recommendation) {
|
|
838
|
+
return { action, recommendation: null, adapted_fields: [] };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const adapted = { ...action };
|
|
842
|
+
const adaptedFields = [];
|
|
843
|
+
const hints = recommendation.hints || {};
|
|
844
|
+
|
|
845
|
+
if (
|
|
846
|
+
typeof hints.preferred_risk_cap === 'number' &&
|
|
847
|
+
(adapted.risk_score === undefined || adapted.risk_score > hints.preferred_risk_cap)
|
|
848
|
+
) {
|
|
849
|
+
adapted.risk_score = hints.preferred_risk_cap;
|
|
850
|
+
adaptedFields.push('risk_score');
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (hints.prefer_reversible === true && adapted.reversible === undefined) {
|
|
854
|
+
adapted.reversible = true;
|
|
855
|
+
adaptedFields.push('reversible');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (
|
|
859
|
+
typeof hints.confidence_floor === 'number' &&
|
|
860
|
+
(adapted.confidence === undefined || adapted.confidence < hints.confidence_floor)
|
|
861
|
+
) {
|
|
862
|
+
adapted.confidence = hints.confidence_floor;
|
|
863
|
+
adaptedFields.push('confidence');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
action: adapted,
|
|
868
|
+
recommendation,
|
|
869
|
+
adapted_fields: adaptedFields,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Create a goal.
|
|
875
875
|
* @param {Object} goal
|
|
876
876
|
* @param {string} goal.title - Goal title
|
|
877
877
|
* @param {string} [goal.category] - Goal category
|
|
@@ -1138,7 +1138,7 @@ class DashClaw {
|
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
1140
|
// ══════════════════════════════════════════════
|
|
1141
|
-
// Category 7: Automation Snippets (
|
|
1141
|
+
// Category 7: Automation Snippets (5 methods)
|
|
1142
1142
|
// ══════════════════════════════════════════════
|
|
1143
1143
|
|
|
1144
1144
|
/**
|
|
@@ -1176,6 +1176,15 @@ class DashClaw {
|
|
|
1176
1176
|
return this._request(`/api/snippets?${params}`, 'GET');
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* Fetch a single snippet by ID.
|
|
1181
|
+
* @param {string} snippetId - The snippet ID
|
|
1182
|
+
* @returns {Promise<{snippet: Object}>}
|
|
1183
|
+
*/
|
|
1184
|
+
async getSnippet(snippetId) {
|
|
1185
|
+
return this._request(`/api/snippets/${snippetId}`, 'GET');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1179
1188
|
/**
|
|
1180
1189
|
* Mark a snippet as used (increments use_count).
|
|
1181
1190
|
* @param {string} snippetId - The snippet ID
|