@usejarvis/brain 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/LICENSE +153 -0
- package/README.md +278 -0
- package/bin/jarvis.ts +413 -0
- package/package.json +74 -0
- package/scripts/ensure-bun.cjs +8 -0
- package/src/actions/README.md +421 -0
- package/src/actions/app-control/desktop-controller.test.ts +26 -0
- package/src/actions/app-control/desktop-controller.ts +438 -0
- package/src/actions/app-control/interface.ts +64 -0
- package/src/actions/app-control/linux.ts +273 -0
- package/src/actions/app-control/macos.ts +54 -0
- package/src/actions/app-control/sidecar-launcher.test.ts +23 -0
- package/src/actions/app-control/sidecar-launcher.ts +286 -0
- package/src/actions/app-control/windows.ts +44 -0
- package/src/actions/browser/cdp.ts +138 -0
- package/src/actions/browser/chrome-launcher.ts +252 -0
- package/src/actions/browser/session.ts +437 -0
- package/src/actions/browser/stealth.ts +49 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/terminal/executor.ts +157 -0
- package/src/actions/terminal/wsl-bridge.ts +126 -0
- package/src/actions/test.ts +93 -0
- package/src/actions/tools/agents.ts +321 -0
- package/src/actions/tools/builtin.ts +846 -0
- package/src/actions/tools/commitments.ts +192 -0
- package/src/actions/tools/content.ts +217 -0
- package/src/actions/tools/delegate.ts +147 -0
- package/src/actions/tools/desktop.test.ts +55 -0
- package/src/actions/tools/desktop.ts +305 -0
- package/src/actions/tools/goals.ts +376 -0
- package/src/actions/tools/local-tools-guard.ts +20 -0
- package/src/actions/tools/registry.ts +171 -0
- package/src/actions/tools/research.ts +111 -0
- package/src/actions/tools/sidecar-list.ts +57 -0
- package/src/actions/tools/sidecar-route.ts +105 -0
- package/src/actions/tools/workflows.ts +216 -0
- package/src/agents/agent.ts +132 -0
- package/src/agents/delegation.ts +107 -0
- package/src/agents/hierarchy.ts +113 -0
- package/src/agents/index.ts +19 -0
- package/src/agents/messaging.ts +125 -0
- package/src/agents/orchestrator.ts +576 -0
- package/src/agents/role-discovery.ts +61 -0
- package/src/agents/sub-agent-runner.ts +307 -0
- package/src/agents/task-manager.ts +151 -0
- package/src/authority/approval-delivery.ts +59 -0
- package/src/authority/approval.ts +196 -0
- package/src/authority/audit.ts +158 -0
- package/src/authority/authority.test.ts +519 -0
- package/src/authority/deferred-executor.ts +103 -0
- package/src/authority/emergency.ts +66 -0
- package/src/authority/engine.ts +297 -0
- package/src/authority/index.ts +12 -0
- package/src/authority/learning.ts +111 -0
- package/src/authority/tool-action-map.ts +74 -0
- package/src/awareness/analytics.ts +466 -0
- package/src/awareness/awareness.test.ts +332 -0
- package/src/awareness/capture-engine.ts +305 -0
- package/src/awareness/context-graph.ts +130 -0
- package/src/awareness/context-tracker.ts +349 -0
- package/src/awareness/index.ts +25 -0
- package/src/awareness/intelligence.ts +321 -0
- package/src/awareness/ocr-engine.ts +88 -0
- package/src/awareness/service.ts +528 -0
- package/src/awareness/struggle-detector.ts +342 -0
- package/src/awareness/suggestion-engine.ts +476 -0
- package/src/awareness/types.ts +201 -0
- package/src/cli/autostart.ts +241 -0
- package/src/cli/deps.ts +449 -0
- package/src/cli/doctor.ts +230 -0
- package/src/cli/helpers.ts +401 -0
- package/src/cli/onboard.ts +580 -0
- package/src/comms/README.md +329 -0
- package/src/comms/auth-error.html +48 -0
- package/src/comms/channels/discord.ts +228 -0
- package/src/comms/channels/signal.ts +56 -0
- package/src/comms/channels/telegram.ts +316 -0
- package/src/comms/channels/whatsapp.ts +60 -0
- package/src/comms/channels.test.ts +173 -0
- package/src/comms/desktop-notify.ts +114 -0
- package/src/comms/example.ts +129 -0
- package/src/comms/index.ts +129 -0
- package/src/comms/streaming.ts +142 -0
- package/src/comms/voice.test.ts +152 -0
- package/src/comms/voice.ts +291 -0
- package/src/comms/websocket.test.ts +409 -0
- package/src/comms/websocket.ts +473 -0
- package/src/config/README.md +387 -0
- package/src/config/index.ts +6 -0
- package/src/config/loader.test.ts +137 -0
- package/src/config/loader.ts +142 -0
- package/src/config/types.ts +260 -0
- package/src/daemon/README.md +232 -0
- package/src/daemon/agent-service-interface.ts +9 -0
- package/src/daemon/agent-service.ts +600 -0
- package/src/daemon/api-routes.ts +2119 -0
- package/src/daemon/background-agent-service.ts +396 -0
- package/src/daemon/background-agent.test.ts +78 -0
- package/src/daemon/channel-service.ts +201 -0
- package/src/daemon/commitment-executor.ts +297 -0
- package/src/daemon/event-classifier.ts +239 -0
- package/src/daemon/event-coalescer.ts +123 -0
- package/src/daemon/event-reactor.ts +214 -0
- package/src/daemon/health.ts +220 -0
- package/src/daemon/index.ts +1004 -0
- package/src/daemon/llm-settings.ts +316 -0
- package/src/daemon/observer-service.ts +150 -0
- package/src/daemon/pid.ts +98 -0
- package/src/daemon/research-queue.ts +155 -0
- package/src/daemon/services.ts +175 -0
- package/src/daemon/ws-service.ts +788 -0
- package/src/goals/accountability.ts +240 -0
- package/src/goals/awareness-bridge.ts +185 -0
- package/src/goals/estimator.ts +185 -0
- package/src/goals/events.ts +28 -0
- package/src/goals/goals.test.ts +400 -0
- package/src/goals/integration.test.ts +329 -0
- package/src/goals/nl-builder.test.ts +220 -0
- package/src/goals/nl-builder.ts +256 -0
- package/src/goals/rhythm.test.ts +177 -0
- package/src/goals/rhythm.ts +275 -0
- package/src/goals/service.test.ts +135 -0
- package/src/goals/service.ts +348 -0
- package/src/goals/types.ts +106 -0
- package/src/goals/workflow-bridge.ts +96 -0
- package/src/integrations/google-api.ts +134 -0
- package/src/integrations/google-auth.ts +175 -0
- package/src/llm/README.md +291 -0
- package/src/llm/anthropic.ts +386 -0
- package/src/llm/gemini.ts +371 -0
- package/src/llm/index.ts +19 -0
- package/src/llm/manager.ts +153 -0
- package/src/llm/ollama.ts +307 -0
- package/src/llm/openai.ts +350 -0
- package/src/llm/provider.test.ts +231 -0
- package/src/llm/provider.ts +60 -0
- package/src/llm/test.ts +87 -0
- package/src/observers/README.md +278 -0
- package/src/observers/calendar.ts +113 -0
- package/src/observers/clipboard.ts +136 -0
- package/src/observers/email.ts +109 -0
- package/src/observers/example.ts +58 -0
- package/src/observers/file-watcher.ts +124 -0
- package/src/observers/index.ts +159 -0
- package/src/observers/notifications.ts +197 -0
- package/src/observers/observers.test.ts +203 -0
- package/src/observers/processes.ts +225 -0
- package/src/personality/README.md +61 -0
- package/src/personality/adapter.ts +196 -0
- package/src/personality/index.ts +20 -0
- package/src/personality/learner.ts +209 -0
- package/src/personality/model.ts +132 -0
- package/src/personality/personality.test.ts +236 -0
- package/src/roles/README.md +252 -0
- package/src/roles/authority.ts +119 -0
- package/src/roles/example-usage.ts +198 -0
- package/src/roles/index.ts +42 -0
- package/src/roles/loader.ts +143 -0
- package/src/roles/prompt-builder.ts +194 -0
- package/src/roles/test-multi.ts +102 -0
- package/src/roles/test-role.yaml +77 -0
- package/src/roles/test-utils.ts +93 -0
- package/src/roles/test.ts +106 -0
- package/src/roles/tool-guide.ts +190 -0
- package/src/roles/types.ts +36 -0
- package/src/roles/utils.ts +200 -0
- package/src/scripts/google-setup.ts +168 -0
- package/src/sidecar/connection.ts +179 -0
- package/src/sidecar/index.ts +6 -0
- package/src/sidecar/manager.ts +542 -0
- package/src/sidecar/protocol.ts +85 -0
- package/src/sidecar/rpc.ts +161 -0
- package/src/sidecar/scheduler.ts +136 -0
- package/src/sidecar/types.ts +112 -0
- package/src/sidecar/validator.ts +144 -0
- package/src/vault/README.md +110 -0
- package/src/vault/awareness.ts +341 -0
- package/src/vault/commitments.ts +299 -0
- package/src/vault/content-pipeline.ts +260 -0
- package/src/vault/conversations.ts +173 -0
- package/src/vault/entities.ts +180 -0
- package/src/vault/extractor.test.ts +356 -0
- package/src/vault/extractor.ts +345 -0
- package/src/vault/facts.ts +190 -0
- package/src/vault/goals.ts +477 -0
- package/src/vault/index.ts +87 -0
- package/src/vault/keychain.ts +99 -0
- package/src/vault/observations.ts +115 -0
- package/src/vault/relationships.ts +178 -0
- package/src/vault/retrieval.test.ts +126 -0
- package/src/vault/retrieval.ts +227 -0
- package/src/vault/schema.ts +658 -0
- package/src/vault/settings.ts +38 -0
- package/src/vault/vectors.ts +92 -0
- package/src/vault/workflows.ts +403 -0
- package/src/workflows/auto-suggest.ts +290 -0
- package/src/workflows/engine.ts +366 -0
- package/src/workflows/events.ts +24 -0
- package/src/workflows/executor.ts +207 -0
- package/src/workflows/nl-builder.ts +198 -0
- package/src/workflows/nodes/actions/agent-task.ts +73 -0
- package/src/workflows/nodes/actions/calendar-action.ts +85 -0
- package/src/workflows/nodes/actions/code-execution.ts +73 -0
- package/src/workflows/nodes/actions/discord.ts +77 -0
- package/src/workflows/nodes/actions/file-write.ts +73 -0
- package/src/workflows/nodes/actions/gmail.ts +69 -0
- package/src/workflows/nodes/actions/http-request.ts +117 -0
- package/src/workflows/nodes/actions/notification.ts +85 -0
- package/src/workflows/nodes/actions/run-tool.ts +55 -0
- package/src/workflows/nodes/actions/send-message.ts +82 -0
- package/src/workflows/nodes/actions/shell-command.ts +76 -0
- package/src/workflows/nodes/actions/telegram.ts +60 -0
- package/src/workflows/nodes/builtin.ts +119 -0
- package/src/workflows/nodes/error/error-handler.ts +37 -0
- package/src/workflows/nodes/error/fallback.ts +47 -0
- package/src/workflows/nodes/error/retry.ts +82 -0
- package/src/workflows/nodes/logic/delay.ts +42 -0
- package/src/workflows/nodes/logic/if-else.ts +41 -0
- package/src/workflows/nodes/logic/loop.ts +90 -0
- package/src/workflows/nodes/logic/merge.ts +38 -0
- package/src/workflows/nodes/logic/race.ts +40 -0
- package/src/workflows/nodes/logic/switch.ts +59 -0
- package/src/workflows/nodes/logic/template-render.ts +53 -0
- package/src/workflows/nodes/logic/variable-get.ts +37 -0
- package/src/workflows/nodes/logic/variable-set.ts +59 -0
- package/src/workflows/nodes/registry.ts +99 -0
- package/src/workflows/nodes/transform/aggregate.ts +99 -0
- package/src/workflows/nodes/transform/csv-parse.ts +70 -0
- package/src/workflows/nodes/transform/json-parse.ts +63 -0
- package/src/workflows/nodes/transform/map-filter.ts +84 -0
- package/src/workflows/nodes/transform/regex-match.ts +89 -0
- package/src/workflows/nodes/triggers/calendar.ts +33 -0
- package/src/workflows/nodes/triggers/clipboard.ts +32 -0
- package/src/workflows/nodes/triggers/cron.ts +40 -0
- package/src/workflows/nodes/triggers/email.ts +40 -0
- package/src/workflows/nodes/triggers/file-change.ts +45 -0
- package/src/workflows/nodes/triggers/git.ts +46 -0
- package/src/workflows/nodes/triggers/manual.ts +23 -0
- package/src/workflows/nodes/triggers/poll.ts +81 -0
- package/src/workflows/nodes/triggers/process.ts +44 -0
- package/src/workflows/nodes/triggers/screen-event.ts +37 -0
- package/src/workflows/nodes/triggers/webhook.ts +39 -0
- package/src/workflows/safe-eval.ts +139 -0
- package/src/workflows/template.ts +118 -0
- package/src/workflows/triggers/cron.ts +311 -0
- package/src/workflows/triggers/manager.ts +285 -0
- package/src/workflows/triggers/observer-bridge.ts +172 -0
- package/src/workflows/triggers/poller.ts +201 -0
- package/src/workflows/triggers/screen-condition.ts +218 -0
- package/src/workflows/triggers/triggers.test.ts +740 -0
- package/src/workflows/triggers/webhook.ts +191 -0
- package/src/workflows/types.ts +133 -0
- package/src/workflows/variables.ts +72 -0
- package/src/workflows/workflows.test.ts +383 -0
- package/src/workflows/yaml.ts +104 -0
- package/ui/dist/index-j75njzc1.css +1199 -0
- package/ui/dist/index-p2zh407q.js +80603 -0
- package/ui/dist/index.html +13 -0
- package/ui/public/openwakeword/models/embedding_model.onnx +0 -0
- package/ui/public/openwakeword/models/hey_jarvis_v0.1.onnx +0 -0
- package/ui/public/openwakeword/models/melspectrogram.onnx +0 -0
- package/ui/public/openwakeword/models/silero_vad.onnx +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.mjs +106 -0
- package/ui/public/ort/ort-wasm-simd-threaded.jsep.wasm +0 -0
- package/ui/public/ort/ort-wasm-simd-threaded.mjs +59 -0
- package/ui/public/ort/ort-wasm-simd-threaded.wasm +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-Agent Runner — Generic LLM+Tool Loop
|
|
3
|
+
*
|
|
4
|
+
* Runs any AgentInstance through the LLM+tool execution loop.
|
|
5
|
+
* Mirrors orchestrator.processMessage() but parameterized on agent
|
|
6
|
+
* instead of hardcoded to primary. Supports progress callbacks for
|
|
7
|
+
* real-time streaming to clients.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AgentInstance } from './agent.ts';
|
|
11
|
+
import type { LLMManager } from '../llm/manager.ts';
|
|
12
|
+
import type { LLMMessage, LLMResponse, LLMToolCall, LLMTool } from '../llm/provider.ts';
|
|
13
|
+
import { ToolRegistry } from '../actions/tools/registry.ts';
|
|
14
|
+
import { toolDefToLLMTool, BUILTIN_TOOLS } from '../actions/tools/builtin.ts';
|
|
15
|
+
import type { ActionCategory } from '../roles/authority.ts';
|
|
16
|
+
import type { AuthorityEngine } from '../authority/engine.ts';
|
|
17
|
+
import type { AuditTrail } from '../authority/audit.ts';
|
|
18
|
+
import type { EmergencyController } from '../authority/emergency.ts';
|
|
19
|
+
import { getActionForTool } from '../authority/tool-action-map.ts';
|
|
20
|
+
|
|
21
|
+
const MAX_TOOL_ITERATIONS = 100; // Lower than primary's 200 — sub-agents should be focused
|
|
22
|
+
const MAX_TOOL_RESULT_CHARS = 6000;
|
|
23
|
+
|
|
24
|
+
export type SubAgentResult = {
|
|
25
|
+
success: boolean;
|
|
26
|
+
response: string;
|
|
27
|
+
toolsUsed: string[];
|
|
28
|
+
tokensUsed: { input: number; output: number };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ProgressCallback = (event: {
|
|
32
|
+
type: 'text' | 'tool_call' | 'done';
|
|
33
|
+
agentName: string;
|
|
34
|
+
agentId: string;
|
|
35
|
+
data: unknown;
|
|
36
|
+
}) => void;
|
|
37
|
+
|
|
38
|
+
export type RunSubAgentOptions = {
|
|
39
|
+
agent: AgentInstance;
|
|
40
|
+
task: string;
|
|
41
|
+
context: string;
|
|
42
|
+
llmManager: LLMManager;
|
|
43
|
+
toolRegistry: ToolRegistry;
|
|
44
|
+
onProgress?: ProgressCallback;
|
|
45
|
+
maxIterations?: number;
|
|
46
|
+
// Authority engine components (optional — if not provided, no gate applied)
|
|
47
|
+
authorityEngine?: AuthorityEngine;
|
|
48
|
+
auditTrail?: AuditTrail;
|
|
49
|
+
emergencyController?: EmergencyController;
|
|
50
|
+
temporaryGrants?: Map<string, ActionCategory[]>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a system prompt for a sub-agent from its role definition.
|
|
55
|
+
*/
|
|
56
|
+
function buildSubAgentPrompt(agent: AgentInstance, context: string): string {
|
|
57
|
+
const role = agent.agent.role;
|
|
58
|
+
|
|
59
|
+
const parts = [
|
|
60
|
+
`You are ${role.name}.`,
|
|
61
|
+
'',
|
|
62
|
+
role.description,
|
|
63
|
+
'',
|
|
64
|
+
'## Your Responsibilities',
|
|
65
|
+
...role.responsibilities.map(r => `- ${r}`),
|
|
66
|
+
'',
|
|
67
|
+
'## Rules',
|
|
68
|
+
'- Focus on completing the specific task assigned to you.',
|
|
69
|
+
'- Use your tools to accomplish the task — don\'t just describe what you would do.',
|
|
70
|
+
'- Be thorough but efficient. Don\'t do unnecessary work.',
|
|
71
|
+
'- Return a clear, structured result when done.',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
if (context) {
|
|
75
|
+
parts.push('', '## Context', context);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parts.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get LLM-formatted tools from a scoped ToolRegistry.
|
|
83
|
+
*/
|
|
84
|
+
function getLLMTools(registry: ToolRegistry): LLMTool[] | undefined {
|
|
85
|
+
if (registry.count() === 0) return undefined;
|
|
86
|
+
return registry.list().map(toolDefToLLMTool);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Execute a single tool call via a ToolRegistry.
|
|
91
|
+
* Includes optional authority gate for sub-agents.
|
|
92
|
+
*/
|
|
93
|
+
async function executeTool(
|
|
94
|
+
registry: ToolRegistry,
|
|
95
|
+
toolCall: LLMToolCall,
|
|
96
|
+
authorityCtx?: {
|
|
97
|
+
agent: AgentInstance;
|
|
98
|
+
engine: AuthorityEngine;
|
|
99
|
+
auditTrail?: AuditTrail;
|
|
100
|
+
emergencyController?: EmergencyController;
|
|
101
|
+
temporaryGrants?: Map<string, ActionCategory[]>;
|
|
102
|
+
}
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
// Authority gate (if engine provided)
|
|
105
|
+
if (authorityCtx) {
|
|
106
|
+
const { agent, engine, auditTrail, emergencyController, temporaryGrants } = authorityCtx;
|
|
107
|
+
|
|
108
|
+
// Emergency check
|
|
109
|
+
if (emergencyController && !emergencyController.canExecute()) {
|
|
110
|
+
return `[SYSTEM ${emergencyController.getState().toUpperCase()}] Tool execution suspended.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tool = registry.get(toolCall.name);
|
|
114
|
+
const actionCategory = getActionForTool(toolCall.name, tool?.category ?? 'unknown');
|
|
115
|
+
|
|
116
|
+
const decision = engine.checkAuthority({
|
|
117
|
+
agentId: agent.id,
|
|
118
|
+
agentAuthorityLevel: agent.agent.authority.max_authority_level,
|
|
119
|
+
agentRoleId: agent.agent.role.id,
|
|
120
|
+
toolName: toolCall.name,
|
|
121
|
+
toolCategory: tool?.category ?? 'unknown',
|
|
122
|
+
actionCategory,
|
|
123
|
+
temporaryGrants: temporaryGrants ?? new Map(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
auditTrail?.log({
|
|
127
|
+
agent_id: agent.id,
|
|
128
|
+
agent_name: agent.agent.role.name,
|
|
129
|
+
tool_name: toolCall.name,
|
|
130
|
+
action_category: actionCategory,
|
|
131
|
+
authority_decision: decision.allowed ? 'allowed' : 'denied',
|
|
132
|
+
executed: decision.allowed,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!decision.allowed) {
|
|
136
|
+
return `[AUTHORITY DENIED] ${toolCall.name}: ${decision.reason}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sub-agents don't get approval flow — they're denied outright for governed actions
|
|
140
|
+
if (decision.requiresApproval) {
|
|
141
|
+
return `[AUTHORITY DENIED] ${toolCall.name} requires user approval. Sub-agents cannot request approvals directly.`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const raw = await registry.execute(toolCall.name, toolCall.arguments);
|
|
147
|
+
let result: string = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
148
|
+
|
|
149
|
+
if (result.length > MAX_TOOL_RESULT_CHARS) {
|
|
150
|
+
result = result.slice(0, MAX_TOOL_RESULT_CHARS) + `\n... (truncated, was ${result.length} chars)`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return `Error executing ${toolCall.name}: ${err instanceof Error ? err.message : String(err)}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Run a sub-agent through the full LLM+tool execution loop.
|
|
161
|
+
*
|
|
162
|
+
* This is the core engine that powers sub-agent execution.
|
|
163
|
+
* It works exactly like the primary agent's processMessage() loop
|
|
164
|
+
* but operates on any AgentInstance with its own scoped tools.
|
|
165
|
+
*/
|
|
166
|
+
export async function runSubAgent(opts: RunSubAgentOptions): Promise<SubAgentResult> {
|
|
167
|
+
const {
|
|
168
|
+
agent,
|
|
169
|
+
task,
|
|
170
|
+
context,
|
|
171
|
+
llmManager,
|
|
172
|
+
toolRegistry,
|
|
173
|
+
onProgress,
|
|
174
|
+
maxIterations = MAX_TOOL_ITERATIONS,
|
|
175
|
+
authorityEngine,
|
|
176
|
+
auditTrail,
|
|
177
|
+
emergencyController,
|
|
178
|
+
temporaryGrants,
|
|
179
|
+
} = opts;
|
|
180
|
+
|
|
181
|
+
// Build authority context if engine provided
|
|
182
|
+
const authorityCtx = authorityEngine ? {
|
|
183
|
+
agent,
|
|
184
|
+
engine: authorityEngine,
|
|
185
|
+
auditTrail,
|
|
186
|
+
emergencyController,
|
|
187
|
+
temporaryGrants,
|
|
188
|
+
} : undefined;
|
|
189
|
+
|
|
190
|
+
const agentName = agent.agent.role.name;
|
|
191
|
+
const agentId = agent.id;
|
|
192
|
+
const toolsUsed: string[] = [];
|
|
193
|
+
const totalUsage = { input: 0, output: 0 };
|
|
194
|
+
|
|
195
|
+
// Set the task on the agent
|
|
196
|
+
agent.setTask(task);
|
|
197
|
+
agent.activate();
|
|
198
|
+
|
|
199
|
+
// Build system prompt
|
|
200
|
+
const systemPrompt = buildSubAgentPrompt(agent, context);
|
|
201
|
+
|
|
202
|
+
// Add the task as a user message
|
|
203
|
+
agent.addMessage('user', task);
|
|
204
|
+
|
|
205
|
+
// Build messages array
|
|
206
|
+
const messages: LLMMessage[] = [
|
|
207
|
+
{ role: 'system', content: systemPrompt },
|
|
208
|
+
...agent.getMessages(),
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const tools = getLLMTools(toolRegistry);
|
|
212
|
+
let finalText = '';
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Tool execution loop
|
|
216
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
217
|
+
const llmResponse: LLMResponse = await llmManager.chat(messages, { tools });
|
|
218
|
+
|
|
219
|
+
totalUsage.input += llmResponse.usage.input_tokens;
|
|
220
|
+
totalUsage.output += llmResponse.usage.output_tokens;
|
|
221
|
+
|
|
222
|
+
if (llmResponse.finish_reason === 'tool_use' && llmResponse.tool_calls.length > 0) {
|
|
223
|
+
// Add assistant message with tool calls
|
|
224
|
+
messages.push({
|
|
225
|
+
role: 'assistant',
|
|
226
|
+
content: llmResponse.content,
|
|
227
|
+
tool_calls: llmResponse.tool_calls,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Notify about text if any
|
|
231
|
+
if (llmResponse.content && onProgress) {
|
|
232
|
+
onProgress({ type: 'text', agentName, agentId, data: llmResponse.content });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Execute each tool
|
|
236
|
+
for (const tc of llmResponse.tool_calls) {
|
|
237
|
+
toolsUsed.push(tc.name);
|
|
238
|
+
|
|
239
|
+
// Notify about tool call
|
|
240
|
+
if (onProgress) {
|
|
241
|
+
onProgress({
|
|
242
|
+
type: 'tool_call',
|
|
243
|
+
agentName,
|
|
244
|
+
agentId,
|
|
245
|
+
data: { name: tc.name, arguments: tc.arguments },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = await executeTool(toolRegistry, tc, authorityCtx);
|
|
250
|
+
messages.push({
|
|
251
|
+
role: 'tool',
|
|
252
|
+
content: result,
|
|
253
|
+
tool_call_id: tc.id,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.log(`[SubAgent:${agentName}] Tool ${tc.name} -> ${result.slice(0, 100)}...`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// No tool calls — this is the final response
|
|
263
|
+
finalText = llmResponse.content;
|
|
264
|
+
|
|
265
|
+
if (onProgress) {
|
|
266
|
+
onProgress({ type: 'text', agentName, agentId, data: finalText });
|
|
267
|
+
onProgress({ type: 'done', agentName, agentId, data: { tokensUsed: totalUsage } });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Add final response to agent's history
|
|
274
|
+
agent.addMessage('assistant', finalText);
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
response: finalText,
|
|
279
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
280
|
+
tokensUsed: totalUsage,
|
|
281
|
+
};
|
|
282
|
+
} catch (err) {
|
|
283
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
284
|
+
console.error(`[SubAgent:${agentName}] Error:`, errorMsg);
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
success: false,
|
|
288
|
+
response: `Sub-agent error: ${errorMsg}`,
|
|
289
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
290
|
+
tokensUsed: totalUsage,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Create a scoped ToolRegistry for a sub-agent.
|
|
297
|
+
* Only includes builtin tools whose category is in the allowed list.
|
|
298
|
+
*/
|
|
299
|
+
export function createScopedToolRegistry(allowedCategories: string[]): ToolRegistry {
|
|
300
|
+
const registry = new ToolRegistry();
|
|
301
|
+
for (const tool of BUILTIN_TOOLS) {
|
|
302
|
+
if (allowedCategories.includes(tool.category)) {
|
|
303
|
+
registry.register(tool);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return registry;
|
|
307
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Task Manager — Background Async Task Runner
|
|
3
|
+
*
|
|
4
|
+
* Manages sub-agent tasks as background Promises. When a task is launched,
|
|
5
|
+
* runSubAgent() fires without blocking — the caller gets a task ID and can
|
|
6
|
+
* check status / collect results later.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runSubAgent, type SubAgentResult, type ProgressCallback } from './sub-agent-runner.ts';
|
|
10
|
+
import type { AgentInstance } from './agent.ts';
|
|
11
|
+
import type { LLMManager } from '../llm/manager.ts';
|
|
12
|
+
import type { ToolRegistry } from '../actions/tools/registry.ts';
|
|
13
|
+
|
|
14
|
+
export type AsyncTaskStatus = 'running' | 'completed' | 'failed';
|
|
15
|
+
|
|
16
|
+
export type AsyncTask = {
|
|
17
|
+
id: string;
|
|
18
|
+
agentId: string;
|
|
19
|
+
agentName: string;
|
|
20
|
+
specialistId: string;
|
|
21
|
+
task: string;
|
|
22
|
+
status: AsyncTaskStatus;
|
|
23
|
+
startedAt: number;
|
|
24
|
+
completedAt: number | null;
|
|
25
|
+
result: SubAgentResult | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LaunchOptions = {
|
|
29
|
+
agent: AgentInstance;
|
|
30
|
+
task: string;
|
|
31
|
+
context: string;
|
|
32
|
+
llmManager: LLMManager;
|
|
33
|
+
toolRegistry: ToolRegistry;
|
|
34
|
+
onProgress?: ProgressCallback;
|
|
35
|
+
onComplete?: (task: AsyncTask) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class AgentTaskManager {
|
|
39
|
+
private tasks = new Map<string, AsyncTask>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Launch a sub-agent task in the background. Returns task ID immediately.
|
|
43
|
+
*/
|
|
44
|
+
launch(opts: LaunchOptions): string {
|
|
45
|
+
const { agent, task, context, llmManager, toolRegistry, onProgress, onComplete } = opts;
|
|
46
|
+
|
|
47
|
+
const taskId = crypto.randomUUID();
|
|
48
|
+
const asyncTask: AsyncTask = {
|
|
49
|
+
id: taskId,
|
|
50
|
+
agentId: agent.id,
|
|
51
|
+
agentName: agent.agent.role.name,
|
|
52
|
+
specialistId: agent.agent.role.id,
|
|
53
|
+
task,
|
|
54
|
+
status: 'running',
|
|
55
|
+
startedAt: Date.now(),
|
|
56
|
+
completedAt: null,
|
|
57
|
+
result: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.tasks.set(taskId, asyncTask);
|
|
61
|
+
|
|
62
|
+
// Fire runSubAgent without awaiting — runs in background
|
|
63
|
+
runSubAgent({
|
|
64
|
+
agent,
|
|
65
|
+
task,
|
|
66
|
+
context,
|
|
67
|
+
llmManager,
|
|
68
|
+
toolRegistry,
|
|
69
|
+
onProgress,
|
|
70
|
+
}).then((result) => {
|
|
71
|
+
asyncTask.status = 'completed';
|
|
72
|
+
asyncTask.completedAt = Date.now();
|
|
73
|
+
asyncTask.result = result;
|
|
74
|
+
console.log(`[TaskManager] Task ${taskId} completed (${asyncTask.agentName})`);
|
|
75
|
+
onComplete?.(asyncTask);
|
|
76
|
+
}).catch((err) => {
|
|
77
|
+
asyncTask.status = 'failed';
|
|
78
|
+
asyncTask.completedAt = Date.now();
|
|
79
|
+
asyncTask.result = {
|
|
80
|
+
success: false,
|
|
81
|
+
response: `Task failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
82
|
+
toolsUsed: [],
|
|
83
|
+
tokensUsed: { input: 0, output: 0 },
|
|
84
|
+
};
|
|
85
|
+
console.error(`[TaskManager] Task ${taskId} failed (${asyncTask.agentName}):`, err);
|
|
86
|
+
onComplete?.(asyncTask);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return taskId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get a task by its ID.
|
|
94
|
+
*/
|
|
95
|
+
getTask(taskId: string): AsyncTask | undefined {
|
|
96
|
+
return this.tasks.get(taskId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find the current/most recent task for an agent.
|
|
101
|
+
*/
|
|
102
|
+
getAgentTask(agentId: string): AsyncTask | undefined {
|
|
103
|
+
let latest: AsyncTask | undefined;
|
|
104
|
+
for (const task of this.tasks.values()) {
|
|
105
|
+
if (task.agentId === agentId) {
|
|
106
|
+
if (!latest || task.startedAt > latest.startedAt) {
|
|
107
|
+
latest = task;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return latest;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check if an agent is currently running a task.
|
|
116
|
+
*/
|
|
117
|
+
isAgentBusy(agentId: string): boolean {
|
|
118
|
+
for (const task of this.tasks.values()) {
|
|
119
|
+
if (task.agentId === agentId && task.status === 'running') {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* List all tasks, optionally filtered by status.
|
|
128
|
+
*/
|
|
129
|
+
listTasks(filter?: { status?: AsyncTaskStatus }): AsyncTask[] {
|
|
130
|
+
const all = Array.from(this.tasks.values());
|
|
131
|
+
if (filter?.status) {
|
|
132
|
+
return all.filter(t => t.status === filter.status);
|
|
133
|
+
}
|
|
134
|
+
return all;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Remove completed/failed tasks older than maxAge (default 10 min).
|
|
139
|
+
*/
|
|
140
|
+
cleanup(maxAgeMs = 10 * 60_000): number {
|
|
141
|
+
let removed = 0;
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
for (const [id, task] of this.tasks) {
|
|
144
|
+
if (task.status !== 'running' && task.completedAt && now - task.completedAt > maxAgeMs) {
|
|
145
|
+
this.tasks.delete(id);
|
|
146
|
+
removed++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return removed;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval Delivery — Pushes approval requests to the user through
|
|
3
|
+
* appropriate channels (WebSocket always, Telegram/Discord if urgent).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ApprovalRequest } from './approval.ts';
|
|
7
|
+
|
|
8
|
+
export type ApprovalBroadcaster = {
|
|
9
|
+
broadcastApprovalRequest(request: ApprovalRequest): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ChannelSender = {
|
|
13
|
+
broadcastToAll(text: string): Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class ApprovalDelivery {
|
|
17
|
+
private broadcaster: ApprovalBroadcaster | null = null;
|
|
18
|
+
private channelSender: ChannelSender | null = null;
|
|
19
|
+
|
|
20
|
+
setBroadcaster(broadcaster: ApprovalBroadcaster): void {
|
|
21
|
+
this.broadcaster = broadcaster;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setChannelSender(sender: ChannelSender): void {
|
|
25
|
+
this.channelSender = sender;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Deliver an approval request to all appropriate channels.
|
|
30
|
+
*/
|
|
31
|
+
async deliver(request: ApprovalRequest): Promise<void> {
|
|
32
|
+
// Always push to dashboard via WebSocket
|
|
33
|
+
this.broadcaster?.broadcastApprovalRequest(request);
|
|
34
|
+
|
|
35
|
+
// If urgent, also push to Telegram/Discord
|
|
36
|
+
if (request.urgency === 'urgent' && this.channelSender) {
|
|
37
|
+
const message = this.formatApprovalMessage(request);
|
|
38
|
+
try {
|
|
39
|
+
await this.channelSender.broadcastToAll(message);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error('[ApprovalDelivery] Failed to send to external channels:', err);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private formatApprovalMessage(request: ApprovalRequest): string {
|
|
47
|
+
const shortId = request.id.slice(0, 8);
|
|
48
|
+
return [
|
|
49
|
+
`[APPROVAL NEEDED]`,
|
|
50
|
+
`Action: ${request.tool_name} (${request.action_category})`,
|
|
51
|
+
`Agent: ${request.agent_name}`,
|
|
52
|
+
`Reason: ${request.reason}`,
|
|
53
|
+
``,
|
|
54
|
+
`Reply with:`,
|
|
55
|
+
` approve ${shortId}`,
|
|
56
|
+
` deny ${shortId}`,
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Approval Manager — Handles the lifecycle of approval requests.
|
|
3
|
+
*
|
|
4
|
+
* Persists to SQLite: pending → approved/denied → executed
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getDb, generateId } from '../vault/schema.ts';
|
|
8
|
+
import type { ActionCategory } from '../roles/authority.ts';
|
|
9
|
+
|
|
10
|
+
export type ApprovalStatus = 'pending' | 'approved' | 'denied' | 'expired' | 'executed';
|
|
11
|
+
export type ApprovalUrgency = 'urgent' | 'normal';
|
|
12
|
+
|
|
13
|
+
export type ApprovalRequest = {
|
|
14
|
+
id: string;
|
|
15
|
+
agent_id: string;
|
|
16
|
+
agent_name: string;
|
|
17
|
+
tool_name: string;
|
|
18
|
+
tool_arguments: string; // JSON string
|
|
19
|
+
action_category: ActionCategory;
|
|
20
|
+
urgency: ApprovalUrgency;
|
|
21
|
+
reason: string;
|
|
22
|
+
context: string;
|
|
23
|
+
status: ApprovalStatus;
|
|
24
|
+
decided_at: number | null;
|
|
25
|
+
decided_by: string | null;
|
|
26
|
+
executed_at: number | null;
|
|
27
|
+
execution_result: string | null;
|
|
28
|
+
created_at: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class ApprovalManager {
|
|
32
|
+
/**
|
|
33
|
+
* Create a new approval request and persist to DB.
|
|
34
|
+
*/
|
|
35
|
+
createRequest(params: {
|
|
36
|
+
agentId: string;
|
|
37
|
+
agentName: string;
|
|
38
|
+
toolName: string;
|
|
39
|
+
toolArguments: Record<string, unknown>;
|
|
40
|
+
actionCategory: ActionCategory;
|
|
41
|
+
urgency: ApprovalUrgency;
|
|
42
|
+
reason: string;
|
|
43
|
+
context: string;
|
|
44
|
+
}): ApprovalRequest {
|
|
45
|
+
const db = getDb();
|
|
46
|
+
const id = generateId();
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const toolArgs = JSON.stringify(params.toolArguments);
|
|
49
|
+
|
|
50
|
+
db.run(
|
|
51
|
+
`INSERT INTO approval_requests (id, agent_id, agent_name, tool_name, tool_arguments, action_category, urgency, reason, context, status, created_at)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
53
|
+
[id, params.agentId, params.agentName, params.toolName, toolArgs, params.actionCategory, params.urgency, params.reason, params.context, now]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
agent_id: params.agentId,
|
|
59
|
+
agent_name: params.agentName,
|
|
60
|
+
tool_name: params.toolName,
|
|
61
|
+
tool_arguments: toolArgs,
|
|
62
|
+
action_category: params.actionCategory as ActionCategory,
|
|
63
|
+
urgency: params.urgency,
|
|
64
|
+
reason: params.reason,
|
|
65
|
+
context: params.context,
|
|
66
|
+
status: 'pending',
|
|
67
|
+
decided_at: null,
|
|
68
|
+
decided_by: null,
|
|
69
|
+
executed_at: null,
|
|
70
|
+
execution_result: null,
|
|
71
|
+
created_at: now,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get a request by ID.
|
|
77
|
+
*/
|
|
78
|
+
getRequest(requestId: string): ApprovalRequest | null {
|
|
79
|
+
const db = getDb();
|
|
80
|
+
const row = db.query('SELECT * FROM approval_requests WHERE id = ?').get(requestId) as ApprovalRequest | null;
|
|
81
|
+
return row;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find a request by short ID prefix (for Telegram/Discord commands).
|
|
86
|
+
*/
|
|
87
|
+
findByShortId(shortId: string): ApprovalRequest | null {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
const row = db.query('SELECT * FROM approval_requests WHERE id LIKE ? AND status = ?')
|
|
90
|
+
.get(`${shortId}%`, 'pending') as ApprovalRequest | null;
|
|
91
|
+
return row;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Approve a pending request.
|
|
96
|
+
*/
|
|
97
|
+
approve(requestId: string, decidedBy: string): ApprovalRequest | null {
|
|
98
|
+
const db = getDb();
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
|
|
101
|
+
const result = db.run(
|
|
102
|
+
`UPDATE approval_requests SET status = 'approved', decided_at = ?, decided_by = ? WHERE id = ? AND status = 'pending'`,
|
|
103
|
+
[now, decidedBy, requestId]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (result.changes === 0) return null;
|
|
107
|
+
return this.getRequest(requestId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Deny a pending request.
|
|
112
|
+
*/
|
|
113
|
+
deny(requestId: string, decidedBy: string): ApprovalRequest | null {
|
|
114
|
+
const db = getDb();
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
|
|
117
|
+
const result = db.run(
|
|
118
|
+
`UPDATE approval_requests SET status = 'denied', decided_at = ?, decided_by = ? WHERE id = ? AND status = 'pending'`,
|
|
119
|
+
[now, decidedBy, requestId]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (result.changes === 0) return null;
|
|
123
|
+
return this.getRequest(requestId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Mark an approved request as executed with its result.
|
|
128
|
+
*/
|
|
129
|
+
markExecuted(requestId: string, executionResult: string): void {
|
|
130
|
+
const db = getDb();
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
|
|
133
|
+
db.run(
|
|
134
|
+
`UPDATE approval_requests SET status = 'executed', executed_at = ?, execution_result = ? WHERE id = ?`,
|
|
135
|
+
[now, executionResult, requestId]
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get all pending requests.
|
|
141
|
+
*/
|
|
142
|
+
getPending(): ApprovalRequest[] {
|
|
143
|
+
const db = getDb();
|
|
144
|
+
return db.query(
|
|
145
|
+
`SELECT * FROM approval_requests WHERE status = 'pending' ORDER BY created_at DESC`
|
|
146
|
+
).all() as ApprovalRequest[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get approval history with optional filters.
|
|
151
|
+
*/
|
|
152
|
+
getHistory(opts?: {
|
|
153
|
+
limit?: number;
|
|
154
|
+
action?: ActionCategory;
|
|
155
|
+
agentId?: string;
|
|
156
|
+
status?: ApprovalStatus;
|
|
157
|
+
}): ApprovalRequest[] {
|
|
158
|
+
const db = getDb();
|
|
159
|
+
const conditions: string[] = [];
|
|
160
|
+
const values: unknown[] = [];
|
|
161
|
+
|
|
162
|
+
if (opts?.action) {
|
|
163
|
+
conditions.push('action_category = ?');
|
|
164
|
+
values.push(opts.action);
|
|
165
|
+
}
|
|
166
|
+
if (opts?.agentId) {
|
|
167
|
+
conditions.push('agent_id = ?');
|
|
168
|
+
values.push(opts.agentId);
|
|
169
|
+
}
|
|
170
|
+
if (opts?.status) {
|
|
171
|
+
conditions.push('status = ?');
|
|
172
|
+
values.push(opts.status);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
176
|
+
const limit = opts?.limit ?? 50;
|
|
177
|
+
|
|
178
|
+
return db.query(
|
|
179
|
+
`SELECT * FROM approval_requests ${where} ORDER BY created_at DESC LIMIT ?`
|
|
180
|
+
).all(...[...values, limit] as any[]) as ApprovalRequest[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Expire old pending requests.
|
|
185
|
+
*/
|
|
186
|
+
expireOld(maxAgeMs: number): number {
|
|
187
|
+
const db = getDb();
|
|
188
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
189
|
+
|
|
190
|
+
const result = db.run(
|
|
191
|
+
`UPDATE approval_requests SET status = 'expired' WHERE status = 'pending' AND created_at < ?`,
|
|
192
|
+
[cutoff]
|
|
193
|
+
);
|
|
194
|
+
return result.changes;
|
|
195
|
+
}
|
|
196
|
+
}
|