Skip to content

Commit 9bf37ff

Browse files
authored
Merge pull request #510 from friggframework/feature/ai-agent-integration
feat(ai-agents): add AI agent integration package with MCP tools
2 parents abe341d + 5561c47 commit 9bf37ff

37 files changed

+6211
-2
lines changed

package-lock.json

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ai-agents/jest.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
testMatch: ['**/tests/**/*.test.js'],
4+
collectCoverageFrom: ['src/**/*.js'],
5+
coverageDirectory: 'coverage',
6+
coverageReporters: ['text', 'lcov'],
7+
verbose: true
8+
};

packages/ai-agents/package.json

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"name": "@friggframework/ai-agents",
3+
"version": "2.0.0-next.0",
4+
"description": "AI agent integration for Frigg Framework",
5+
"main": "src/index.js",
6+
"author": "",
7+
"license": "MIT",
8+
"repository": {
9+
"type": "git",
10+
"url": "git+https://github.com/friggframework/frigg.git"
11+
},
12+
"bugs": {
13+
"url": "https://github.com/friggframework/frigg/issues"
14+
},
15+
"homepage": "https://github.com/friggframework/frigg#readme",
16+
"publishConfig": {
17+
"access": "public"
18+
},
19+
"scripts": {
20+
"test": "jest",
21+
"test:watch": "jest --watch",
22+
"test:coverage": "jest --coverage",
23+
"lint:fix": "prettier --write --loglevel error . && eslint . --fix"
24+
},
25+
"keywords": [
26+
"frigg",
27+
"ai",
28+
"agents",
29+
"mcp",
30+
"llm"
31+
],
32+
"dependencies": {
33+
"@friggframework/schemas": "^2.0.0-next.0"
34+
},
35+
"peerDependencies": {
36+
"ai": ">=4.0.0",
37+
"@ai-sdk/openai": ">=1.0.0",
38+
"@anthropic-ai/claude-agent-sdk": ">=0.1.0"
39+
},
40+
"peerDependenciesMeta": {
41+
"ai": {
42+
"optional": true
43+
},
44+
"@ai-sdk/openai": {
45+
"optional": true
46+
},
47+
"@anthropic-ai/claude-agent-sdk": {
48+
"optional": true
49+
}
50+
},
51+
"devDependencies": {
52+
"@friggframework/eslint-config": "^2.0.0-next.0",
53+
"@friggframework/prettier-config": "^2.0.0-next.0",
54+
"jest": "^29.7.0",
55+
"eslint": "^8.22.0",
56+
"prettier": "^2.7.1"
57+
},
58+
"prettier": "@friggframework/prettier-config"
59+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const AgentEventType = {
2+
CONTENT: 'content',
3+
TOOL_CALL: 'tool_call',
4+
TOOL_RESULT: 'tool_result',
5+
USAGE: 'usage',
6+
DONE: 'done',
7+
ERROR: 'error'
8+
};
9+
10+
class AgentEvent {
11+
constructor({ type, timestamp = new Date(), ...data }) {
12+
this.type = type;
13+
this.timestamp = timestamp;
14+
Object.assign(this, data);
15+
}
16+
17+
static content(text) {
18+
return new AgentEvent({ type: AgentEventType.CONTENT, content: text });
19+
}
20+
21+
static toolCall(name, args) {
22+
return new AgentEvent({ type: AgentEventType.TOOL_CALL, name, args });
23+
}
24+
25+
static toolResult(name, result) {
26+
return new AgentEvent({ type: AgentEventType.TOOL_RESULT, name, result });
27+
}
28+
29+
static usage(usage) {
30+
return new AgentEvent({ type: AgentEventType.USAGE, usage });
31+
}
32+
33+
static done() {
34+
return new AgentEvent({ type: AgentEventType.DONE });
35+
}
36+
37+
static error(error) {
38+
return new AgentEvent({ type: AgentEventType.ERROR, error });
39+
}
40+
41+
toJSON() {
42+
return {
43+
type: this.type,
44+
timestamp: this.timestamp.toISOString(),
45+
...(this.content && { content: this.content }),
46+
...(this.name && { name: this.name }),
47+
...(this.args && { args: this.args }),
48+
...(this.result && { result: this.result }),
49+
...(this.usage && { usage: this.usage }),
50+
...(this.error && { error: this.error.message || String(this.error) })
51+
};
52+
}
53+
}
54+
55+
module.exports = { AgentEvent, AgentEventType };
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const ProposalStatus = {
2+
PENDING: 'pending',
3+
APPROVED: 'approved',
4+
REJECTED: 'rejected',
5+
MODIFIED: 'modified'
6+
};
7+
8+
class AgentProposal {
9+
constructor({ id, files, validation, checkpointId = null }) {
10+
this.id = id;
11+
this.files = files;
12+
this.validation = validation;
13+
this.checkpointId = checkpointId;
14+
this.status = ProposalStatus.PENDING;
15+
this.createdAt = new Date();
16+
this.approvedAt = null;
17+
this.rejectedAt = null;
18+
this.rejectionReason = null;
19+
}
20+
21+
getSummary() {
22+
let linesAdded = 0;
23+
let createdFiles = 0;
24+
let modifiedFiles = 0;
25+
26+
for (const file of this.files) {
27+
if (file.action === 'create') {
28+
createdFiles++;
29+
if (file.content) {
30+
linesAdded += file.content.split('\n').length;
31+
}
32+
} else if (file.action === 'modify') {
33+
modifiedFiles++;
34+
}
35+
}
36+
37+
return {
38+
fileCount: this.files.length,
39+
createdFiles,
40+
modifiedFiles,
41+
linesAdded,
42+
confidence: this.validation?.confidence,
43+
recommendation: this.validation?.recommendation
44+
};
45+
}
46+
47+
approve() {
48+
if (this.status === ProposalStatus.REJECTED) {
49+
throw new Error('Cannot approve rejected proposal');
50+
}
51+
this.status = ProposalStatus.APPROVED;
52+
this.approvedAt = new Date();
53+
}
54+
55+
reject(reason) {
56+
this.status = ProposalStatus.REJECTED;
57+
this.rejectedAt = new Date();
58+
this.rejectionReason = reason;
59+
}
60+
61+
canRollback() {
62+
return this.checkpointId !== null;
63+
}
64+
65+
toJSON() {
66+
return {
67+
id: this.id,
68+
status: this.status,
69+
files: this.files.map(f => ({ path: f.path, action: f.action })),
70+
validation: this.validation,
71+
checkpointId: this.checkpointId,
72+
summary: this.getSummary(),
73+
createdAt: this.createdAt.toISOString(),
74+
approvedAt: this.approvedAt?.toISOString(),
75+
rejectedAt: this.rejectedAt?.toISOString(),
76+
rejectionReason: this.rejectionReason
77+
};
78+
}
79+
}
80+
81+
module.exports = { AgentProposal, ProposalStatus };
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { AgentEvent, AgentEventType } = require('./agent-event');
2+
const { AgentProposal, ProposalStatus } = require('./agent-proposal');
3+
4+
module.exports = {
5+
AgentEvent,
6+
AgentEventType,
7+
AgentProposal,
8+
ProposalStatus
9+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const interfaces = require('./interfaces');
2+
const entities = require('./entities');
3+
4+
module.exports = {
5+
...interfaces,
6+
...entities
7+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class NotImplementedError extends Error {
2+
constructor(method) {
3+
super(`Not implemented: ${method}`);
4+
this.name = 'NotImplementedError';
5+
}
6+
}
7+
8+
class IAgentFramework {
9+
async runAgent(_params) {
10+
throw new NotImplementedError('runAgent');
11+
}
12+
13+
async loadMcpTools(_serverConfig) {
14+
throw new NotImplementedError('loadMcpTools');
15+
}
16+
17+
getCapabilities() {
18+
throw new NotImplementedError('getCapabilities');
19+
}
20+
21+
async validateRunParams(params) {
22+
if (!params.prompt) {
23+
throw new Error('prompt is required');
24+
}
25+
return true;
26+
}
27+
}
28+
29+
module.exports = { IAgentFramework, NotImplementedError };
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const { IAgentFramework, NotImplementedError } = require('./agent-framework');
2+
const { IValidationPipeline, WEIGHTS, THRESHOLDS } = require('./validation-pipeline');
3+
4+
module.exports = {
5+
IAgentFramework,
6+
IValidationPipeline,
7+
NotImplementedError,
8+
WEIGHTS,
9+
THRESHOLDS
10+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const { NotImplementedError } = require('./agent-framework');
2+
3+
const WEIGHTS = {
4+
schema: 0.30,
5+
patterns: 0.25,
6+
security: 0.15,
7+
tests: 0.20,
8+
lint: 0.10
9+
};
10+
11+
const THRESHOLDS = {
12+
autoApprove: 95,
13+
requireReview: 80
14+
};
15+
16+
class IValidationPipeline {
17+
async validate(_files) {
18+
throw new NotImplementedError('validate');
19+
}
20+
21+
calculateConfidence(layerResults) {
22+
let totalScore = 0;
23+
24+
for (const [layer, weight] of Object.entries(WEIGHTS)) {
25+
const result = layerResults[layer];
26+
if (result && typeof result.score === 'number') {
27+
totalScore += result.score * weight;
28+
}
29+
}
30+
31+
return Math.round(totalScore);
32+
}
33+
34+
getRecommendation(confidence, thresholds = THRESHOLDS) {
35+
if (confidence >= thresholds.autoApprove) {
36+
return 'auto_approve';
37+
}
38+
if (confidence >= thresholds.requireReview) {
39+
return 'require_review';
40+
}
41+
return 'manual_approval';
42+
}
43+
44+
generateFeedback(layerResults) {
45+
const feedback = [];
46+
47+
for (const [layer, result] of Object.entries(layerResults)) {
48+
if (!result.passed) {
49+
feedback.push({
50+
layer,
51+
score: result.score,
52+
issues: result.errors || result.violations || result.vulnerabilities || result.failures || []
53+
});
54+
}
55+
}
56+
57+
return feedback;
58+
}
59+
}
60+
61+
module.exports = { IValidationPipeline, WEIGHTS, THRESHOLDS };

0 commit comments

Comments
 (0)