botinabox 2.5.0 → 2.5.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/LICENSE +21 -21
- package/README.md +190 -190
- package/bin/botinabox.mjs +2 -2
- package/dist/channels/discord/adapter.d.ts +32 -0
- package/dist/channels/discord/adapter.js +70 -0
- package/dist/channels/discord/inbound.d.ts +25 -0
- package/dist/channels/discord/inbound.js +24 -0
- package/dist/channels/discord/models.d.ts +8 -0
- package/dist/channels/discord/models.js +5 -0
- package/dist/channels/discord/outbound.d.ts +14 -0
- package/dist/channels/discord/outbound.js +38 -0
- package/dist/channels/slack/adapter.d.ts +33 -0
- package/dist/channels/slack/adapter.js +74 -0
- package/dist/channels/slack/inbound.d.ts +59 -0
- package/dist/channels/slack/inbound.js +96 -0
- package/dist/channels/slack/models.d.ts +9 -0
- package/dist/channels/slack/models.js +5 -0
- package/dist/channels/slack/outbound.d.ts +12 -0
- package/dist/channels/slack/outbound.js +18 -0
- package/dist/channels/slack/transcribe.d.ts +41 -0
- package/dist/channels/slack/transcribe.js +106 -0
- package/dist/channels/webhook/adapter.d.ts +23 -0
- package/dist/channels/webhook/adapter.js +86 -0
- package/dist/channels/webhook/hmac.d.ts +13 -0
- package/dist/channels/webhook/hmac.js +26 -0
- package/dist/channels/webhook/models.d.ts +9 -0
- package/dist/channels/webhook/models.js +5 -0
- package/dist/channels/webhook/server.d.ts +20 -0
- package/dist/channels/webhook/server.js +91 -0
- package/dist/chat-pipeline-BWrtVqEP.d.ts +652 -0
- package/dist/chat-pipeline-C-XlLGNl.d.ts +648 -0
- package/dist/chat-pipeline-CR1KF6eX.d.ts +652 -0
- package/dist/chat-pipeline-DisuC8SB.d.ts +643 -0
- package/dist/chunk-2LGXQPEA.js +41 -0
- package/dist/chunk-3X3YKI4T.js +357 -0
- package/dist/chunk-D47AIFOD.js +351 -0
- package/dist/chunk-DSNJKNEW.js +328 -0
- package/dist/chunk-GS2JFL6I.js +144 -0
- package/dist/chunk-J6S6QMUY.js +144 -0
- package/dist/chunk-QLA6YOFN.js +22 -0
- package/dist/chunk-UACT2WXX.js +381 -0
- package/dist/cli/templates/config.yml.d.ts +7 -0
- package/dist/cli/templates/config.yml.js +61 -0
- package/dist/cli/templates/env.d.ts +1 -0
- package/dist/cli/templates/env.js +30 -0
- package/dist/cli/templates/index.ts.d.ts +2 -0
- package/dist/cli/templates/index.ts.js +30 -0
- package/dist/cli/templates/package.json.d.ts +5 -0
- package/dist/cli/templates/package.json.js +28 -0
- package/dist/cli.js +0 -0
- package/dist/connector-DDahQw-2.d.ts +63 -0
- package/dist/connectors/google/calendar-connector.d.ts +40 -0
- package/dist/connectors/google/calendar-connector.js +243 -0
- package/dist/connectors/google/gmail-connector.d.ts +42 -0
- package/dist/connectors/google/gmail-connector.js +345 -0
- package/dist/connectors/google/index.d.ts +67 -1
- package/dist/connectors/google/index.js +240 -0
- package/dist/connectors/google/oauth.d.ts +48 -0
- package/dist/connectors/google/oauth.js +112 -0
- package/dist/connectors/google/types.d.ts +78 -0
- package/dist/connectors/google/types.js +2 -0
- package/dist/core/chat/auto-discovery.d.ts +16 -0
- package/dist/core/chat/auto-discovery.js +54 -0
- package/dist/core/chat/channel-registry.d.ts +45 -0
- package/dist/core/chat/channel-registry.js +96 -0
- package/dist/core/chat/chat-pipeline.d.ts +113 -0
- package/dist/core/chat/chat-pipeline.js +395 -0
- package/dist/core/chat/chat-responder.d.ts +90 -0
- package/dist/core/chat/chat-responder.js +185 -0
- package/dist/core/chat/formatter.d.ts +11 -0
- package/dist/core/chat/formatter.js +60 -0
- package/dist/core/chat/index.d.ts +24 -0
- package/dist/core/chat/index.js +18 -0
- package/dist/core/chat/message-interpreter.d.ts +91 -0
- package/dist/core/chat/message-interpreter.js +166 -0
- package/dist/core/chat/message-store.d.ts +66 -0
- package/dist/core/chat/message-store.js +131 -0
- package/dist/core/chat/notification-queue.d.ts +34 -0
- package/dist/core/chat/notification-queue.js +111 -0
- package/dist/core/chat/pipeline.d.ts +38 -0
- package/dist/core/chat/pipeline.js +89 -0
- package/dist/core/chat/policies.d.ts +16 -0
- package/dist/core/chat/policies.js +25 -0
- package/dist/core/chat/routing.d.ts +17 -0
- package/dist/core/chat/routing.js +36 -0
- package/dist/core/chat/session-key.d.ts +30 -0
- package/dist/core/chat/session-key.js +65 -0
- package/dist/core/chat/session-manager.d.ts +17 -0
- package/dist/core/chat/session-manager.js +23 -0
- package/dist/core/chat/text-chunker.d.ts +9 -0
- package/dist/core/chat/text-chunker.js +48 -0
- package/dist/core/chat/triage-router.d.ts +75 -0
- package/dist/core/chat/triage-router.js +142 -0
- package/dist/core/chat/types.d.ts +5 -0
- package/dist/core/chat/types.js +5 -0
- package/dist/core/config/defaults.d.ts +2 -0
- package/dist/core/config/defaults.js +38 -0
- package/dist/core/config/index.d.ts +6 -0
- package/dist/core/config/index.js +4 -0
- package/dist/core/config/interpolate.d.ts +5 -0
- package/dist/core/config/interpolate.js +27 -0
- package/dist/core/config/loader.d.ts +24 -0
- package/dist/core/config/loader.js +59 -0
- package/dist/core/config/schema.d.ts +5 -0
- package/dist/core/config/schema.js +119 -0
- package/dist/core/data/core-entity-contexts.d.ts +14 -0
- package/dist/core/data/core-entity-contexts.js +197 -0
- package/dist/core/data/core-migrations.d.ts +5 -0
- package/dist/core/data/core-migrations.js +45 -0
- package/dist/core/data/core-schema.d.ts +6 -0
- package/dist/core/data/core-schema.js +454 -0
- package/dist/core/data/data-store.d.ts +67 -0
- package/dist/core/data/data-store.js +218 -0
- package/dist/core/data/domain-entity-contexts.d.ts +29 -0
- package/dist/core/data/domain-entity-contexts.js +321 -0
- package/dist/core/data/domain-schema.d.ts +36 -0
- package/dist/core/data/domain-schema.js +323 -0
- package/dist/core/data/index.d.ts +7 -0
- package/dist/core/data/index.js +7 -0
- package/dist/core/data/types.d.ts +111 -0
- package/dist/core/data/types.js +1 -0
- package/dist/core/hooks/hook-bus.d.ts +18 -0
- package/dist/core/hooks/hook-bus.js +120 -0
- package/dist/core/hooks/index.d.ts +2 -0
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/types.d.ts +19 -0
- package/dist/core/hooks/types.js +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +4 -0
- package/dist/core/llm/auto-discovery.d.ts +11 -0
- package/dist/core/llm/auto-discovery.js +49 -0
- package/dist/core/llm/cost-tracker.d.ts +6 -0
- package/dist/core/llm/cost-tracker.js +38 -0
- package/dist/core/llm/index.d.ts +4 -0
- package/dist/core/llm/index.js +3 -0
- package/dist/core/llm/model-router.d.ts +25 -0
- package/dist/core/llm/model-router.js +49 -0
- package/dist/core/llm/provider-registry.d.ts +9 -0
- package/dist/core/llm/provider-registry.js +25 -0
- package/dist/core/llm/types.d.ts +2 -0
- package/dist/core/llm/types.js +2 -0
- package/dist/core/orchestrator/adapters/api-adapter.d.ts +34 -0
- package/dist/core/orchestrator/adapters/api-adapter.js +88 -0
- package/dist/core/orchestrator/adapters/cli-adapter.d.ts +22 -0
- package/dist/core/orchestrator/adapters/cli-adapter.js +69 -0
- package/dist/core/orchestrator/adapters/deterministic-adapter.d.ts +35 -0
- package/dist/core/orchestrator/adapters/deterministic-adapter.js +75 -0
- package/dist/core/orchestrator/adapters/env-whitelist.d.ts +4 -0
- package/dist/core/orchestrator/adapters/env-whitelist.js +27 -0
- package/dist/core/orchestrator/adapters/output-extractor.d.ts +11 -0
- package/dist/core/orchestrator/adapters/output-extractor.js +59 -0
- package/dist/core/orchestrator/adapters/process-manager.d.ts +15 -0
- package/dist/core/orchestrator/adapters/process-manager.js +26 -0
- package/dist/core/orchestrator/adapters/tool-loop.d.ts +22 -0
- package/dist/core/orchestrator/adapters/tool-loop.js +66 -0
- package/dist/core/orchestrator/agent-registry.d.ts +31 -0
- package/dist/core/orchestrator/agent-registry.js +135 -0
- package/dist/core/orchestrator/budget-controller.d.ts +19 -0
- package/dist/core/orchestrator/budget-controller.js +73 -0
- package/dist/core/orchestrator/chain-guard.d.ts +14 -0
- package/dist/core/orchestrator/chain-guard.js +23 -0
- package/dist/core/orchestrator/circuit-breaker.d.ts +65 -0
- package/dist/core/orchestrator/circuit-breaker.js +159 -0
- package/dist/core/orchestrator/claude-stream-parser.d.ts +31 -0
- package/dist/core/orchestrator/claude-stream-parser.js +99 -0
- package/dist/core/orchestrator/config-revisions.d.ts +6 -0
- package/dist/core/orchestrator/config-revisions.js +17 -0
- package/dist/core/orchestrator/dependency-resolver.d.ts +20 -0
- package/dist/core/orchestrator/dependency-resolver.js +78 -0
- package/dist/core/orchestrator/governance-gate.d.ts +110 -0
- package/dist/core/orchestrator/governance-gate.js +170 -0
- package/dist/core/orchestrator/learning-pipeline.d.ts +109 -0
- package/dist/core/orchestrator/learning-pipeline.js +249 -0
- package/dist/core/orchestrator/loop-detector.d.ts +51 -0
- package/dist/core/orchestrator/loop-detector.js +133 -0
- package/dist/core/orchestrator/ndjson-logger.d.ts +6 -0
- package/dist/core/orchestrator/ndjson-logger.js +18 -0
- package/dist/core/orchestrator/permission-relay.d.ts +72 -0
- package/dist/core/orchestrator/permission-relay.js +164 -0
- package/dist/core/orchestrator/run-manager.d.ts +31 -0
- package/dist/core/orchestrator/run-manager.js +178 -0
- package/dist/core/orchestrator/scheduler.d.ts +70 -0
- package/dist/core/orchestrator/scheduler.js +198 -0
- package/dist/core/orchestrator/secret-store.d.ts +57 -0
- package/dist/core/orchestrator/secret-store.js +171 -0
- package/dist/core/orchestrator/session-manager.d.ts +13 -0
- package/dist/core/orchestrator/session-manager.js +66 -0
- package/dist/core/orchestrator/task-queue.d.ts +34 -0
- package/dist/core/orchestrator/task-queue.js +83 -0
- package/dist/core/orchestrator/template-interpolate.d.ts +5 -0
- package/dist/core/orchestrator/template-interpolate.js +18 -0
- package/dist/core/orchestrator/user-registry.d.ts +47 -0
- package/dist/core/orchestrator/user-registry.js +76 -0
- package/dist/core/orchestrator/wakeup-queue.d.ts +9 -0
- package/dist/core/orchestrator/wakeup-queue.js +45 -0
- package/dist/core/orchestrator/workflow-engine.d.ts +47 -0
- package/dist/core/orchestrator/workflow-engine.js +204 -0
- package/dist/core/security/audit.d.ts +20 -0
- package/dist/core/security/audit.js +33 -0
- package/dist/core/security/column-validator.d.ts +20 -0
- package/dist/core/security/column-validator.js +37 -0
- package/dist/core/security/index.d.ts +5 -0
- package/dist/core/security/index.js +5 -0
- package/dist/core/security/process-env.d.ts +13 -0
- package/dist/core/security/process-env.js +49 -0
- package/dist/core/security/sanitizer.d.ts +11 -0
- package/dist/core/security/sanitizer.js +39 -0
- package/dist/core/security/types.d.ts +11 -0
- package/dist/core/security/types.js +1 -0
- package/dist/core/update/auto-update.d.ts +21 -0
- package/dist/core/update/auto-update.js +102 -0
- package/dist/core/update/backup-manager.d.ts +7 -0
- package/dist/core/update/backup-manager.js +24 -0
- package/dist/core/update/index.d.ts +8 -0
- package/dist/core/update/index.js +6 -0
- package/dist/core/update/migration-hooks.d.ts +11 -0
- package/dist/core/update/migration-hooks.js +10 -0
- package/dist/core/update/types.d.ts +11 -0
- package/dist/core/update/types.js +1 -0
- package/dist/core/update/update-checker.d.ts +11 -0
- package/dist/core/update/update-checker.js +63 -0
- package/dist/core/update/update-manager.d.ts +25 -0
- package/dist/core/update/update-manager.js +101 -0
- package/dist/core/update/version-utils.d.ts +6 -0
- package/dist/core/update/version-utils.js +34 -0
- package/dist/gmail-connector-2FVYTQJH.js +6 -0
- package/dist/gmail-connector-MNUBRNFM.js +6 -0
- package/dist/gmail-connector-PS2VLGNE.js +6 -0
- package/dist/gmail-connector-ULSMN6X2.js +6 -0
- package/dist/gmail-connector-URRFX6A3.js +6 -0
- package/dist/inbound-AFBUPSPG.js +10 -0
- package/dist/inbound-AFOHYNUY.js +6 -0
- package/dist/inbound-CGIXRXGC.js +8 -0
- package/dist/inbound-MCOLRH6U.js +10 -0
- package/dist/inbound-SNEMBLGA.js +6 -0
- package/dist/inbound-ZJHAYVMF.js +10 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +27 -11
- package/dist/provider-qqJYv9nv.d.ts +75 -0
- package/dist/providers/anthropic/models.d.ts +2 -0
- package/dist/providers/anthropic/models.js +29 -0
- package/dist/providers/anthropic/provider.d.ts +13 -0
- package/dist/providers/anthropic/provider.js +119 -0
- package/dist/providers/anthropic/tool-converter.d.ts +10 -0
- package/dist/providers/anthropic/tool-converter.js +7 -0
- package/dist/providers/ollama/provider.d.ts +17 -0
- package/dist/providers/ollama/provider.js +185 -0
- package/dist/providers/openai/models.d.ts +2 -0
- package/dist/providers/openai/models.js +29 -0
- package/dist/providers/openai/provider.d.ts +13 -0
- package/dist/providers/openai/provider.js +163 -0
- package/dist/providers/openai/tool-converter.d.ts +10 -0
- package/dist/providers/openai/tool-converter.js +10 -0
- package/dist/shared/constants.d.ts +50 -0
- package/dist/shared/constants.js +64 -0
- package/dist/shared/index.d.ts +14 -0
- package/dist/shared/index.js +14 -0
- package/dist/shared/types/agent.d.ts +36 -0
- package/dist/shared/types/agent.js +2 -0
- package/dist/shared/types/channel.d.ts +70 -0
- package/dist/shared/types/channel.js +2 -0
- package/dist/shared/types/config.d.ts +111 -0
- package/dist/shared/types/config.js +2 -0
- package/dist/shared/types/connector.d.ts +77 -0
- package/dist/shared/types/connector.js +2 -0
- package/dist/shared/types/execution.d.ts +29 -0
- package/dist/shared/types/execution.js +2 -0
- package/dist/shared/types/provider.d.ts +73 -0
- package/dist/shared/types/provider.js +2 -0
- package/dist/shared/types/task.d.ts +47 -0
- package/dist/shared/types/task.js +2 -0
- package/dist/shared/types/workflow.d.ts +39 -0
- package/dist/shared/types/workflow.js +2 -0
- package/dist/shared/utils.d.ts +6 -0
- package/dist/shared/utils.js +13 -0
- package/dist/update-check.d.ts +5 -0
- package/dist/update-check.js +56 -0
- package/package.json +100 -100
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LearningPipeline — turns execution experience into durable knowledge.
|
|
3
|
+
* Story 6.5
|
|
4
|
+
*
|
|
5
|
+
* Promotion ladder:
|
|
6
|
+
* Execution → Feedback (structured capture)
|
|
7
|
+
* → 3+ similar → Playbook (generalized rule)
|
|
8
|
+
* → 3+ projects → Skill (executable behavior)
|
|
9
|
+
* → Agent-Skill Matrix → Per-Agent Context
|
|
10
|
+
*
|
|
11
|
+
* Two-axis evaluation:
|
|
12
|
+
* - Accuracy: was the output correct?
|
|
13
|
+
* - Efficiency: how fast / how many tokens?
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_PLAYBOOK_THRESHOLD = 3;
|
|
16
|
+
const DEFAULT_SKILL_THRESHOLD = 3;
|
|
17
|
+
export class LearningPipeline {
|
|
18
|
+
db;
|
|
19
|
+
hooks;
|
|
20
|
+
playbookThreshold;
|
|
21
|
+
skillThreshold;
|
|
22
|
+
autoPromote;
|
|
23
|
+
constructor(db, hooks, config) {
|
|
24
|
+
this.db = db;
|
|
25
|
+
this.hooks = hooks;
|
|
26
|
+
this.playbookThreshold = config?.playbookThreshold ?? DEFAULT_PLAYBOOK_THRESHOLD;
|
|
27
|
+
this.skillThreshold = config?.skillThreshold ?? DEFAULT_SKILL_THRESHOLD;
|
|
28
|
+
this.autoPromote = config?.autoPromote ?? false;
|
|
29
|
+
}
|
|
30
|
+
// --- Feedback Layer ---
|
|
31
|
+
/**
|
|
32
|
+
* Capture a structured feedback record from an execution.
|
|
33
|
+
*/
|
|
34
|
+
async captureFeedback(entry) {
|
|
35
|
+
const row = await this.db.insert('feedback', {
|
|
36
|
+
agent_id: entry.agentId,
|
|
37
|
+
task_id: entry.taskId,
|
|
38
|
+
issue: entry.issue,
|
|
39
|
+
root_cause: entry.rootCause,
|
|
40
|
+
severity: entry.severity,
|
|
41
|
+
repeatable: entry.repeatable ? 1 : 0,
|
|
42
|
+
accuracy_score: entry.accuracyScore,
|
|
43
|
+
efficiency_score: entry.efficiencyScore,
|
|
44
|
+
tags: JSON.stringify(entry.tags ?? []),
|
|
45
|
+
});
|
|
46
|
+
const feedbackId = row['id'];
|
|
47
|
+
await this.hooks.emit('learning.feedback_captured', {
|
|
48
|
+
feedbackId,
|
|
49
|
+
agentId: entry.agentId,
|
|
50
|
+
issue: entry.issue,
|
|
51
|
+
severity: entry.severity,
|
|
52
|
+
});
|
|
53
|
+
// Check for auto-promotion
|
|
54
|
+
if (this.autoPromote) {
|
|
55
|
+
await this.checkPlaybookPromotion(entry.issue);
|
|
56
|
+
}
|
|
57
|
+
return feedbackId;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get all feedback records, optionally filtered.
|
|
61
|
+
*/
|
|
62
|
+
async listFeedback(filter) {
|
|
63
|
+
const where = {};
|
|
64
|
+
if (filter?.agentId)
|
|
65
|
+
where['agent_id'] = filter.agentId;
|
|
66
|
+
if (filter?.severity)
|
|
67
|
+
where['severity'] = filter.severity;
|
|
68
|
+
if (filter?.repeatable !== undefined)
|
|
69
|
+
where['repeatable'] = filter.repeatable ? 1 : 0;
|
|
70
|
+
return this.db.query('feedback', Object.keys(where).length ? { where } : undefined);
|
|
71
|
+
}
|
|
72
|
+
// --- Playbook Layer ---
|
|
73
|
+
/**
|
|
74
|
+
* Check if feedback records with similar issues should be promoted to a playbook.
|
|
75
|
+
* Groups by issue text similarity (exact match for now).
|
|
76
|
+
*/
|
|
77
|
+
async checkPlaybookPromotion(issue) {
|
|
78
|
+
const allFeedback = await this.db.query('feedback', {
|
|
79
|
+
where: { issue },
|
|
80
|
+
});
|
|
81
|
+
if (allFeedback.length < this.playbookThreshold) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
// Check if a playbook already exists for this pattern
|
|
85
|
+
const existingPlaybooks = await this.db.query('playbooks', {
|
|
86
|
+
where: { pattern: issue },
|
|
87
|
+
});
|
|
88
|
+
if (existingPlaybooks.length > 0) {
|
|
89
|
+
return existingPlaybooks[0]['id'];
|
|
90
|
+
}
|
|
91
|
+
// Auto-promote: create a playbook from the feedback
|
|
92
|
+
const feedbackIds = allFeedback.map((f) => f['id']);
|
|
93
|
+
const rootCauses = allFeedback
|
|
94
|
+
.map((f) => f['root_cause'])
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
const rule = rootCauses.length > 0
|
|
97
|
+
? `When encountering "${issue}": ${rootCauses[0]}`
|
|
98
|
+
: `Pattern detected: "${issue}" — review and add specific guidance.`;
|
|
99
|
+
const playbookId = await this.promoteToPlaybook({
|
|
100
|
+
pattern: issue,
|
|
101
|
+
rule,
|
|
102
|
+
feedbackIds,
|
|
103
|
+
projectScoped: true,
|
|
104
|
+
});
|
|
105
|
+
return playbookId;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Manually create a playbook from a set of feedback records.
|
|
109
|
+
*/
|
|
110
|
+
async promoteToPlaybook(entry) {
|
|
111
|
+
const row = await this.db.insert('playbooks', {
|
|
112
|
+
pattern: entry.pattern,
|
|
113
|
+
rule: entry.rule,
|
|
114
|
+
feedback_ids: JSON.stringify(entry.feedbackIds),
|
|
115
|
+
project_scoped: entry.projectScoped ? 1 : 0,
|
|
116
|
+
});
|
|
117
|
+
const playbookId = row['id'];
|
|
118
|
+
// Link to agents if specified
|
|
119
|
+
if (entry.agentIds) {
|
|
120
|
+
for (const agentId of entry.agentIds) {
|
|
121
|
+
await this.db.insert('agent_playbooks', {
|
|
122
|
+
agent_id: agentId,
|
|
123
|
+
playbook_id: playbookId,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
await this.hooks.emit('learning.playbook_promoted', {
|
|
128
|
+
playbookId,
|
|
129
|
+
pattern: entry.pattern,
|
|
130
|
+
feedbackCount: entry.feedbackIds.length,
|
|
131
|
+
});
|
|
132
|
+
return playbookId;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* List playbooks, optionally filtered.
|
|
136
|
+
*/
|
|
137
|
+
async listPlaybooks(filter) {
|
|
138
|
+
const where = {};
|
|
139
|
+
if (filter?.projectScoped !== undefined) {
|
|
140
|
+
where['project_scoped'] = filter.projectScoped ? 1 : 0;
|
|
141
|
+
}
|
|
142
|
+
return this.db.query('playbooks', Object.keys(where).length ? { where } : undefined);
|
|
143
|
+
}
|
|
144
|
+
// --- Skill Layer ---
|
|
145
|
+
/**
|
|
146
|
+
* Check if a playbook should be promoted to a skill.
|
|
147
|
+
* A playbook becomes a skill when it works across multiple projects
|
|
148
|
+
* (indicated by being referenced by agents in different contexts).
|
|
149
|
+
*/
|
|
150
|
+
async checkSkillPromotion(playbookId) {
|
|
151
|
+
const playbook = await this.db.get('playbooks', { id: playbookId });
|
|
152
|
+
if (!playbook)
|
|
153
|
+
return undefined;
|
|
154
|
+
// Count distinct agents using this playbook
|
|
155
|
+
const links = await this.db.query('agent_playbooks', {
|
|
156
|
+
where: { playbook_id: playbookId },
|
|
157
|
+
});
|
|
158
|
+
if (links.length < this.skillThreshold) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
// Check if skill already exists for this pattern
|
|
162
|
+
const pattern = playbook['pattern'];
|
|
163
|
+
const existingSkills = await this.db.query('skills', {
|
|
164
|
+
where: { name: pattern },
|
|
165
|
+
});
|
|
166
|
+
if (existingSkills.length > 0) {
|
|
167
|
+
return existingSkills[0]['id'];
|
|
168
|
+
}
|
|
169
|
+
// Promote to skill
|
|
170
|
+
const slug = pattern
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
173
|
+
.replace(/^-|-$/g, '')
|
|
174
|
+
.slice(0, 64);
|
|
175
|
+
const skillId = await this.promoteToSkill({
|
|
176
|
+
name: pattern,
|
|
177
|
+
slug,
|
|
178
|
+
description: `Auto-promoted from playbook: ${pattern}`,
|
|
179
|
+
behavior: playbook['rule'],
|
|
180
|
+
sourcePlaybookIds: [playbookId],
|
|
181
|
+
});
|
|
182
|
+
return skillId;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Manually promote a playbook to a reusable skill.
|
|
186
|
+
*/
|
|
187
|
+
async promoteToSkill(entry) {
|
|
188
|
+
const row = await this.db.insert('skills', {
|
|
189
|
+
name: entry.name,
|
|
190
|
+
slug: entry.slug,
|
|
191
|
+
description: entry.description,
|
|
192
|
+
category: entry.category ?? 'learned',
|
|
193
|
+
definition: JSON.stringify({
|
|
194
|
+
behavior: entry.behavior,
|
|
195
|
+
source_playbook_ids: entry.sourcePlaybookIds,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
const skillId = row['id'];
|
|
199
|
+
await this.hooks.emit('learning.skill_promoted', {
|
|
200
|
+
skillId,
|
|
201
|
+
name: entry.name,
|
|
202
|
+
slug: entry.slug,
|
|
203
|
+
sourcePlaybookCount: entry.sourcePlaybookIds.length,
|
|
204
|
+
});
|
|
205
|
+
return skillId;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Assign a skill to an agent.
|
|
209
|
+
*/
|
|
210
|
+
async assignSkill(agentId, skillId) {
|
|
211
|
+
await this.db.link('agent_skills', {
|
|
212
|
+
agent_id: agentId,
|
|
213
|
+
skill_id: skillId,
|
|
214
|
+
});
|
|
215
|
+
await this.hooks.emit('learning.skill_assigned', { agentId, skillId });
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get learning metrics for an agent.
|
|
219
|
+
*/
|
|
220
|
+
async getMetrics(agentId) {
|
|
221
|
+
const feedback = await this.db.query('feedback', { where: { agent_id: agentId } });
|
|
222
|
+
const accuracyScores = feedback
|
|
223
|
+
.map((f) => f['accuracy_score'])
|
|
224
|
+
.filter((s) => s !== null && s !== undefined);
|
|
225
|
+
const efficiencyScores = feedback
|
|
226
|
+
.map((f) => f['efficiency_score'])
|
|
227
|
+
.filter((s) => s !== null && s !== undefined);
|
|
228
|
+
let playbookCount = 0;
|
|
229
|
+
try {
|
|
230
|
+
const links = await this.db.query('agent_playbooks', { where: { agent_id: agentId } });
|
|
231
|
+
playbookCount = links.length;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Table may not exist
|
|
235
|
+
}
|
|
236
|
+
const skillLinks = await this.db.query('agent_skills', { where: { agent_id: agentId } });
|
|
237
|
+
return {
|
|
238
|
+
feedbackCount: feedback.length,
|
|
239
|
+
avgAccuracy: accuracyScores.length > 0
|
|
240
|
+
? accuracyScores.reduce((a, b) => a + b, 0) / accuracyScores.length
|
|
241
|
+
: null,
|
|
242
|
+
avgEfficiency: efficiencyScores.length > 0
|
|
243
|
+
? efficiencyScores.reduce((a, b) => a + b, 0) / efficiencyScores.length
|
|
244
|
+
: null,
|
|
245
|
+
playbookCount,
|
|
246
|
+
skillCount: skillLinks.length,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoopDetector — pattern-based loop detection for agent routing.
|
|
3
|
+
* Story 6.2
|
|
4
|
+
*
|
|
5
|
+
* Complements chain-guard's depth limit with active pattern detection:
|
|
6
|
+
* - Self-loop: agent routes a task back to itself
|
|
7
|
+
* - Ping-pong: two agents bounce tasks between each other (A→B→A→B)
|
|
8
|
+
* - Blocked re-entry: a task re-enters the system after being blocked
|
|
9
|
+
*/
|
|
10
|
+
import type { DataStore } from '../data/data-store.js';
|
|
11
|
+
export declare enum LoopType {
|
|
12
|
+
SELF_LOOP = "self_loop",
|
|
13
|
+
PING_PONG = "ping_pong",
|
|
14
|
+
BLOCKED_REENTRY = "blocked_reentry"
|
|
15
|
+
}
|
|
16
|
+
export interface LoopDetection {
|
|
17
|
+
type: LoopType;
|
|
18
|
+
agents: string[];
|
|
19
|
+
taskId: string;
|
|
20
|
+
chainOriginId?: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
export interface LoopDetectorConfig {
|
|
24
|
+
/** Number of recent followup records to scan. Default: 10 */
|
|
25
|
+
windowSize?: number;
|
|
26
|
+
/** Minimum repetitions to confirm ping-pong. Default: 2 */
|
|
27
|
+
pingPongThreshold?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare class LoopDetector {
|
|
30
|
+
private db;
|
|
31
|
+
private readonly windowSize;
|
|
32
|
+
private readonly pingPongThreshold;
|
|
33
|
+
constructor(db: DataStore, config?: LoopDetectorConfig);
|
|
34
|
+
/**
|
|
35
|
+
* Check for loops before creating a followup task.
|
|
36
|
+
* Returns a LoopDetection if a loop pattern is found, undefined otherwise.
|
|
37
|
+
*/
|
|
38
|
+
check(sourceAgentId: string, targetAgentId: string, taskId: string, chainOriginId?: string): Promise<LoopDetection | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if an agent is routing to itself.
|
|
41
|
+
*/
|
|
42
|
+
private checkSelfLoop;
|
|
43
|
+
/**
|
|
44
|
+
* Check if a previously blocked task is being re-entered.
|
|
45
|
+
*/
|
|
46
|
+
private checkBlockedReentry;
|
|
47
|
+
/**
|
|
48
|
+
* Check for A→B→A→B ping-pong by scanning recent tasks in the chain.
|
|
49
|
+
*/
|
|
50
|
+
private checkPingPong;
|
|
51
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoopDetector — pattern-based loop detection for agent routing.
|
|
3
|
+
* Story 6.2
|
|
4
|
+
*
|
|
5
|
+
* Complements chain-guard's depth limit with active pattern detection:
|
|
6
|
+
* - Self-loop: agent routes a task back to itself
|
|
7
|
+
* - Ping-pong: two agents bounce tasks between each other (A→B→A→B)
|
|
8
|
+
* - Blocked re-entry: a task re-enters the system after being blocked
|
|
9
|
+
*/
|
|
10
|
+
export var LoopType;
|
|
11
|
+
(function (LoopType) {
|
|
12
|
+
LoopType["SELF_LOOP"] = "self_loop";
|
|
13
|
+
LoopType["PING_PONG"] = "ping_pong";
|
|
14
|
+
LoopType["BLOCKED_REENTRY"] = "blocked_reentry";
|
|
15
|
+
})(LoopType || (LoopType = {}));
|
|
16
|
+
const DEFAULT_WINDOW = 10;
|
|
17
|
+
const DEFAULT_PING_PONG_THRESHOLD = 2;
|
|
18
|
+
export class LoopDetector {
|
|
19
|
+
db;
|
|
20
|
+
windowSize;
|
|
21
|
+
pingPongThreshold;
|
|
22
|
+
constructor(db, config) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
this.windowSize = config?.windowSize ?? DEFAULT_WINDOW;
|
|
25
|
+
this.pingPongThreshold = config?.pingPongThreshold ?? DEFAULT_PING_PONG_THRESHOLD;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check for loops before creating a followup task.
|
|
29
|
+
* Returns a LoopDetection if a loop pattern is found, undefined otherwise.
|
|
30
|
+
*/
|
|
31
|
+
async check(sourceAgentId, targetAgentId, taskId, chainOriginId) {
|
|
32
|
+
// 1. Self-loop: source == target
|
|
33
|
+
const selfLoop = this.checkSelfLoop(sourceAgentId, targetAgentId, taskId);
|
|
34
|
+
if (selfLoop)
|
|
35
|
+
return selfLoop;
|
|
36
|
+
// 2. Blocked re-entry
|
|
37
|
+
const blocked = await this.checkBlockedReentry(targetAgentId, taskId, chainOriginId);
|
|
38
|
+
if (blocked)
|
|
39
|
+
return blocked;
|
|
40
|
+
// 3. Ping-pong: look at recent chain history
|
|
41
|
+
const pingPong = await this.checkPingPong(sourceAgentId, targetAgentId, chainOriginId);
|
|
42
|
+
if (pingPong)
|
|
43
|
+
return pingPong;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if an agent is routing to itself.
|
|
48
|
+
*/
|
|
49
|
+
checkSelfLoop(sourceAgentId, targetAgentId, taskId) {
|
|
50
|
+
if (sourceAgentId === targetAgentId) {
|
|
51
|
+
return {
|
|
52
|
+
type: LoopType.SELF_LOOP,
|
|
53
|
+
agents: [sourceAgentId],
|
|
54
|
+
taskId,
|
|
55
|
+
message: `Self-loop detected: agent ${sourceAgentId} is routing to itself`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if a previously blocked task is being re-entered.
|
|
62
|
+
*/
|
|
63
|
+
async checkBlockedReentry(targetAgentId, taskId, chainOriginId) {
|
|
64
|
+
// Look for blocked/failed tasks in the same chain
|
|
65
|
+
const originId = chainOriginId ?? taskId;
|
|
66
|
+
const chainTasks = await this.db.query('tasks', {
|
|
67
|
+
where: { chain_origin_id: originId },
|
|
68
|
+
});
|
|
69
|
+
const blockedInChain = chainTasks.filter((t) => (t['status'] === 'blocked' || t['status'] === 'failed') &&
|
|
70
|
+
t['assignee_id'] === targetAgentId);
|
|
71
|
+
if (blockedInChain.length > 0) {
|
|
72
|
+
return {
|
|
73
|
+
type: LoopType.BLOCKED_REENTRY,
|
|
74
|
+
agents: [targetAgentId],
|
|
75
|
+
taskId,
|
|
76
|
+
chainOriginId: originId,
|
|
77
|
+
message: `Blocked re-entry: agent ${targetAgentId} already has a blocked/failed task in chain ${originId}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check for A→B→A→B ping-pong by scanning recent tasks in the chain.
|
|
84
|
+
*/
|
|
85
|
+
async checkPingPong(sourceAgentId, targetAgentId, chainOriginId) {
|
|
86
|
+
if (!chainOriginId)
|
|
87
|
+
return undefined;
|
|
88
|
+
// Get recent tasks in this chain, ordered by creation
|
|
89
|
+
const chainTasks = await this.db.query('tasks', {
|
|
90
|
+
where: { chain_origin_id: chainOriginId },
|
|
91
|
+
});
|
|
92
|
+
// Sort by chain_depth (ascending) then created_at
|
|
93
|
+
const sorted = chainTasks
|
|
94
|
+
.sort((a, b) => {
|
|
95
|
+
const depthDiff = (a['chain_depth'] ?? 0) - (b['chain_depth'] ?? 0);
|
|
96
|
+
if (depthDiff !== 0)
|
|
97
|
+
return depthDiff;
|
|
98
|
+
return (a['created_at'] ?? '').localeCompare(b['created_at'] ?? '');
|
|
99
|
+
})
|
|
100
|
+
.slice(-this.windowSize);
|
|
101
|
+
// Extract the agent sequence
|
|
102
|
+
const agentSequence = sorted
|
|
103
|
+
.map((t) => t['assignee_id'])
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
// Add the proposed next hop
|
|
106
|
+
agentSequence.push(targetAgentId);
|
|
107
|
+
// Detect A→B→A→B pattern
|
|
108
|
+
if (agentSequence.length >= this.pingPongThreshold * 2) {
|
|
109
|
+
const tail = agentSequence.slice(-this.pingPongThreshold * 2);
|
|
110
|
+
const a = tail[0];
|
|
111
|
+
const b = tail[1];
|
|
112
|
+
if (a && b && a !== b) {
|
|
113
|
+
let isPingPong = true;
|
|
114
|
+
for (let i = 0; i < tail.length; i++) {
|
|
115
|
+
if (tail[i] !== (i % 2 === 0 ? a : b)) {
|
|
116
|
+
isPingPong = false;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (isPingPong) {
|
|
121
|
+
return {
|
|
122
|
+
type: LoopType.PING_PONG,
|
|
123
|
+
agents: [a, b],
|
|
124
|
+
taskId: sorted[sorted.length - 1]?.['id'] ?? '',
|
|
125
|
+
chainOriginId,
|
|
126
|
+
message: `Ping-pong detected: agents ${a} and ${b} are bouncing tasks in chain ${chainOriginId}`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
export class NdjsonLogger {
|
|
3
|
+
logPath;
|
|
4
|
+
constructor(logPath) {
|
|
5
|
+
this.logPath = logPath;
|
|
6
|
+
}
|
|
7
|
+
log(stream, chunk) {
|
|
8
|
+
const line = JSON.stringify({
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
stream,
|
|
11
|
+
chunk,
|
|
12
|
+
});
|
|
13
|
+
appendFileSync(this.logPath, line + '\n', 'utf8');
|
|
14
|
+
}
|
|
15
|
+
close() {
|
|
16
|
+
// No-op — synchronous writes flush immediately
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionRelay — remote approval for unattended agent execution.
|
|
3
|
+
* Story 6.6
|
|
4
|
+
*
|
|
5
|
+
* When an agent needs human approval but the operator is away:
|
|
6
|
+
* 1. Post the approval prompt to a messaging platform (Slack, Discord, etc.)
|
|
7
|
+
* 2. Poll for response (approve/deny)
|
|
8
|
+
* 3. Relay the decision back to the agent
|
|
9
|
+
*
|
|
10
|
+
* Dual approval: local terminal + remote messaging. First response wins.
|
|
11
|
+
* Race condition handled by atomic state transition.
|
|
12
|
+
*/
|
|
13
|
+
import type { HookBus } from '../hooks/hook-bus.js';
|
|
14
|
+
export type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'expired';
|
|
15
|
+
export interface PermissionPrompt {
|
|
16
|
+
id: string;
|
|
17
|
+
agentId: string;
|
|
18
|
+
action: string;
|
|
19
|
+
context?: string;
|
|
20
|
+
requestedAt: string;
|
|
21
|
+
expiresAt?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ApprovalResponse {
|
|
24
|
+
promptId: string;
|
|
25
|
+
status: 'approved' | 'denied';
|
|
26
|
+
respondedBy: string;
|
|
27
|
+
respondedAt: string;
|
|
28
|
+
comment?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Provider interface — implement for each messaging platform.
|
|
32
|
+
*/
|
|
33
|
+
export interface PermissionProvider {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
/** Post an approval request, return a handle for polling. */
|
|
36
|
+
sendPrompt(prompt: PermissionPrompt): Promise<string>;
|
|
37
|
+
/** Check for a response. Returns undefined if still pending. */
|
|
38
|
+
pollResponse(handle: string): Promise<ApprovalResponse | undefined>;
|
|
39
|
+
/** Cancel a pending prompt (e.g. after local approval). */
|
|
40
|
+
cancelPrompt(handle: string): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
export interface PermissionRelayConfig {
|
|
43
|
+
/** Registered providers (e.g. Slack, Discord adapters) */
|
|
44
|
+
providers: PermissionProvider[];
|
|
45
|
+
/** Poll interval in ms. Default: 5000 */
|
|
46
|
+
pollIntervalMs?: number;
|
|
47
|
+
/** Timeout for pending approvals in ms. Default: 300_000 (5 min) */
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
}
|
|
50
|
+
export declare class PermissionRelay {
|
|
51
|
+
private hooks;
|
|
52
|
+
private readonly providers;
|
|
53
|
+
private readonly pollIntervalMs;
|
|
54
|
+
private readonly timeoutMs;
|
|
55
|
+
private readonly pending;
|
|
56
|
+
constructor(hooks: HookBus, config: PermissionRelayConfig);
|
|
57
|
+
/**
|
|
58
|
+
* Request approval from all configured providers.
|
|
59
|
+
* Returns when the first provider responds (approve or deny).
|
|
60
|
+
*/
|
|
61
|
+
requestApproval(prompt: PermissionPrompt): Promise<ApprovalResponse>;
|
|
62
|
+
/**
|
|
63
|
+
* Provide a local approval (from terminal).
|
|
64
|
+
* Resolves the pending request and cancels remote providers.
|
|
65
|
+
*/
|
|
66
|
+
approveLocally(promptId: string, approved: boolean): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Get all pending approval requests.
|
|
69
|
+
*/
|
|
70
|
+
getPending(): PermissionPrompt[];
|
|
71
|
+
private cancelOtherProviders;
|
|
72
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionRelay — remote approval for unattended agent execution.
|
|
3
|
+
* Story 6.6
|
|
4
|
+
*
|
|
5
|
+
* When an agent needs human approval but the operator is away:
|
|
6
|
+
* 1. Post the approval prompt to a messaging platform (Slack, Discord, etc.)
|
|
7
|
+
* 2. Poll for response (approve/deny)
|
|
8
|
+
* 3. Relay the decision back to the agent
|
|
9
|
+
*
|
|
10
|
+
* Dual approval: local terminal + remote messaging. First response wins.
|
|
11
|
+
* Race condition handled by atomic state transition.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1_000;
|
|
15
|
+
export class PermissionRelay {
|
|
16
|
+
hooks;
|
|
17
|
+
providers;
|
|
18
|
+
pollIntervalMs;
|
|
19
|
+
timeoutMs;
|
|
20
|
+
pending = new Map();
|
|
21
|
+
constructor(hooks, config) {
|
|
22
|
+
this.hooks = hooks;
|
|
23
|
+
this.providers = config.providers;
|
|
24
|
+
this.pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
25
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Request approval from all configured providers.
|
|
29
|
+
* Returns when the first provider responds (approve or deny).
|
|
30
|
+
*/
|
|
31
|
+
async requestApproval(prompt) {
|
|
32
|
+
// Set expiry
|
|
33
|
+
const expiresAt = new Date(Date.now() + this.timeoutMs).toISOString();
|
|
34
|
+
const promptWithExpiry = { ...prompt, expiresAt };
|
|
35
|
+
await this.hooks.emit('permission.requested', {
|
|
36
|
+
promptId: prompt.id,
|
|
37
|
+
agentId: prompt.agentId,
|
|
38
|
+
action: prompt.action,
|
|
39
|
+
});
|
|
40
|
+
// Send to all providers
|
|
41
|
+
const handles = new Map();
|
|
42
|
+
for (const provider of this.providers) {
|
|
43
|
+
try {
|
|
44
|
+
const handle = await provider.sendPrompt(promptWithExpiry);
|
|
45
|
+
handles.set(provider.id, handle);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Provider unavailable — continue with others
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (handles.size === 0) {
|
|
52
|
+
throw new Error('No permission providers available');
|
|
53
|
+
}
|
|
54
|
+
// Race: poll all providers, first response wins
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const entry = {
|
|
57
|
+
prompt: promptWithExpiry,
|
|
58
|
+
handles,
|
|
59
|
+
resolve,
|
|
60
|
+
reject,
|
|
61
|
+
};
|
|
62
|
+
this.pending.set(prompt.id, entry);
|
|
63
|
+
// Start polling
|
|
64
|
+
const pollTimer = setInterval(async () => {
|
|
65
|
+
for (const [providerId, handle] of handles) {
|
|
66
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
67
|
+
if (!provider)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
const response = await provider.pollResponse(handle);
|
|
71
|
+
if (response) {
|
|
72
|
+
clearInterval(pollTimer);
|
|
73
|
+
clearTimeout(timeoutTimer);
|
|
74
|
+
this.pending.delete(prompt.id);
|
|
75
|
+
// Cancel remaining providers
|
|
76
|
+
await this.cancelOtherProviders(handles, providerId);
|
|
77
|
+
await this.hooks.emit('permission.responded', {
|
|
78
|
+
promptId: prompt.id,
|
|
79
|
+
status: response.status,
|
|
80
|
+
respondedBy: response.respondedBy,
|
|
81
|
+
});
|
|
82
|
+
resolve(response);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Poll error — continue
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, this.pollIntervalMs);
|
|
91
|
+
// Timeout
|
|
92
|
+
const timeoutTimer = setTimeout(async () => {
|
|
93
|
+
clearInterval(pollTimer);
|
|
94
|
+
this.pending.delete(prompt.id);
|
|
95
|
+
// Cancel all providers
|
|
96
|
+
for (const [providerId, handle] of handles) {
|
|
97
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
98
|
+
if (provider) {
|
|
99
|
+
try {
|
|
100
|
+
await provider.cancelPrompt(handle);
|
|
101
|
+
}
|
|
102
|
+
catch { /* ignore */ }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
await this.hooks.emit('permission.expired', {
|
|
106
|
+
promptId: prompt.id,
|
|
107
|
+
agentId: prompt.agentId,
|
|
108
|
+
});
|
|
109
|
+
reject(new Error(`Permission request expired after ${this.timeoutMs}ms`));
|
|
110
|
+
}, this.timeoutMs);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Provide a local approval (from terminal).
|
|
115
|
+
* Resolves the pending request and cancels remote providers.
|
|
116
|
+
*/
|
|
117
|
+
async approveLocally(promptId, approved) {
|
|
118
|
+
const entry = this.pending.get(promptId);
|
|
119
|
+
if (!entry)
|
|
120
|
+
return;
|
|
121
|
+
const response = {
|
|
122
|
+
promptId,
|
|
123
|
+
status: approved ? 'approved' : 'denied',
|
|
124
|
+
respondedBy: 'local',
|
|
125
|
+
respondedAt: new Date().toISOString(),
|
|
126
|
+
};
|
|
127
|
+
this.pending.delete(promptId);
|
|
128
|
+
// Cancel all remote providers
|
|
129
|
+
for (const [providerId, handle] of entry.handles) {
|
|
130
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
131
|
+
if (provider) {
|
|
132
|
+
try {
|
|
133
|
+
await provider.cancelPrompt(handle);
|
|
134
|
+
}
|
|
135
|
+
catch { /* ignore */ }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await this.hooks.emit('permission.responded', {
|
|
139
|
+
promptId,
|
|
140
|
+
status: response.status,
|
|
141
|
+
respondedBy: 'local',
|
|
142
|
+
});
|
|
143
|
+
entry.resolve(response);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get all pending approval requests.
|
|
147
|
+
*/
|
|
148
|
+
getPending() {
|
|
149
|
+
return Array.from(this.pending.values()).map((e) => e.prompt);
|
|
150
|
+
}
|
|
151
|
+
async cancelOtherProviders(handles, excludeProviderId) {
|
|
152
|
+
for (const [providerId, handle] of handles) {
|
|
153
|
+
if (providerId === excludeProviderId)
|
|
154
|
+
continue;
|
|
155
|
+
const provider = this.providers.find((p) => p.id === providerId);
|
|
156
|
+
if (provider) {
|
|
157
|
+
try {
|
|
158
|
+
await provider.cancelPrompt(handle);
|
|
159
|
+
}
|
|
160
|
+
catch { /* ignore */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|