Skip to content

Commit 8c01306

Browse files
fullsend-ai-coder[bot]gabemonteroclaude
authored
feat(#3307): add boost-backend-module-kagenti provider module (#3573)
* feat(#3307): add boost-backend-module-kagenti provider module Create the Kagenti provider module as an independent createBackendModule with pluginId 'boost' and moduleId 'kagenti'. The module registers KagentiProvider via the boostProviderExtensionPoint, enabling Kagenti as an AI backend using the A2A protocol. Implementation includes: - KagentiProvider implementing AgenticProvider with A2A protocol integration for chat and streaming - KagentiProviderFactory reading connection config from boost.providers.kagenti and creating all caches - AgentCardCache using cacheService with 5m TTL (task 3.3) - KeycloakTokenCache with dynamic TTL per token (task 3.4) - SessionMap for A2A sessions with 24h TTL (task 3.7) - Kagenti-specific types (agent cards, A2A protocol, namespaces) defined in the module only (task 3b.1) - No cross-provider imports (task 2.5) All caches use Backstage coreServices.cache per Decision 3. No raw Map<> caches. 25 tests passing with coverage. Closes #3307 * fix: address review — canceled state, chat fetch guard, cache object handling - Handle 'canceled' task status in normalizeStreamEvent (yields error event) - Wrap chat() fetch in try/catch for consistent error handling with chatStream() - Handle pre-parsed objects from cache backends in AgentCardCache and SessionMap - Add tests for all three fixes (31 tests passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use unknown cast for cache JsonValue to domain type conversions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: annotate entity provider composition as deferred to issue #3308 The provider-packaging spec referenced catalogProcessingExtensionPoint entity provider composition in both module registration scenarios. That work is tracked in the agent-creation-discovery change (issue #3308), not in the provider module issues. Added cross-reference annotations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: check task status in chat(), add done after failed/canceled, validate TTL - chat() now throws on failed/canceled A2A task status instead of returning empty string - normalizeStreamEvent yields done after failed/canceled for symmetry with completed state (stream terminates immediately) - KeycloakTokenCache.set() skips cache for non-positive TTL values - Added tests: chat() failed/canceled, chatStream failed, TTL validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Co-authored-by: gabemontero <gmontero@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4bf51cc commit 8c01306

18 files changed

Lines changed: 2006 additions & 4 deletions

File tree

workspaces/boost/openspec/changes/pluggable-ai-platform-architecture/specs/provider-packaging/spec.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ The Llama Stack provider MUST be packaged as an independent Backstage backend mo
1515
- **WHEN** the `plugin-boost-backend-module-llamastack` package is installed
1616
- **THEN** it creates a `createBackendModule` with `pluginId: 'boost'` and `moduleId: 'llamastack'`
1717
- **AND** it registers `ResponsesApiProviderFactory` via `boostProviderExtensionPoint` (AI capabilities — composable, accessed by the core backend)
18-
- **AND** it composes `llamastack-entity-provider` internally, registering its entity providers via `catalogProcessingExtensionPoint`
19-
- **AND** `llamastack-entity-provider` is also independently deployable as its own RHDH dynamic plugin (without boost)
18+
- **AND** it composes `llamastack-entity-provider` internally, registering its entity providers via `catalogProcessingExtensionPoint` _(deferred to `agent-creation-discovery` change, issue #3308)_
19+
- **AND** `llamastack-entity-provider` is also independently deployable as its own RHDH dynamic plugin (without boost) _(deferred to `agent-creation-discovery` change, issue #3308)_
2020
- **AND** it depends on `coreServices.config` for Llama Stack connection settings
2121
- **AND** it depends on `coreServices.logger` and `coreServices.cache` for operational services
2222

@@ -47,8 +47,8 @@ The Kagenti provider MUST be packaged as an independent Backstage backend module
4747
- **WHEN** the `plugin-boost-backend-module-kagenti` package is installed
4848
- **THEN** it creates a `createBackendModule` with `pluginId: 'boost'` and `moduleId: 'kagenti'`
4949
- **AND** it registers `KagentiProviderFactory` via `boostProviderExtensionPoint` (AI capabilities — composable, accessed by the core backend)
50-
- **AND** it composes `kagenti-entity-provider` internally, registering its entity providers via `catalogProcessingExtensionPoint`
51-
- **AND** `kagenti-entity-provider` is also independently deployable as its own RHDH dynamic plugin (without boost)
50+
- **AND** it composes `kagenti-entity-provider` internally, registering its entity providers via `catalogProcessingExtensionPoint` _(deferred to `agent-creation-discovery` change, issue #3308)_
51+
- **AND** `kagenti-entity-provider` is also independently deployable as its own RHDH dynamic plugin (without boost) _(deferred to `agent-creation-discovery` change, issue #3308)_
5252
- **AND** it depends on `coreServices.config` for Kagenti/Keycloak connection settings
5353
- **AND** it depends on `coreServices.cache` for agent card caching and token management
5454

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"name": "@red-hat-developer-hub/backstage-plugin-boost-backend-module-kagenti",
3+
"version": "0.1.0",
4+
"license": "Apache-2.0",
5+
"description": "Kagenti provider module for the boost AI platform",
6+
"main": "src/index.ts",
7+
"types": "src/index.ts",
8+
"publishConfig": {
9+
"access": "public"
10+
},
11+
"backstage": {
12+
"role": "backend-plugin-module",
13+
"pluginId": "boost",
14+
"pluginPackage": "@red-hat-developer-hub/backstage-plugin-boost-backend",
15+
"pluginPackages": [
16+
"@red-hat-developer-hub/backstage-plugin-boost-backend",
17+
"@red-hat-developer-hub/backstage-plugin-boost-backend-module-kagenti",
18+
"@red-hat-developer-hub/backstage-plugin-boost-common",
19+
"@red-hat-developer-hub/backstage-plugin-boost-node"
20+
]
21+
},
22+
"exports": {
23+
".": "./src/index.ts",
24+
"./package.json": "./package.json"
25+
},
26+
"typesVersions": {
27+
"*": {
28+
"package.json": [
29+
"package.json"
30+
]
31+
}
32+
},
33+
"dependencies": {
34+
"@backstage/backend-plugin-api": "^1.9.1",
35+
"@red-hat-developer-hub/backstage-plugin-boost-common": "workspace:^",
36+
"@red-hat-developer-hub/backstage-plugin-boost-node": "workspace:^"
37+
},
38+
"devDependencies": {
39+
"@backstage/cli": "^0.34.5"
40+
},
41+
"sideEffects": false,
42+
"scripts": {
43+
"build": "backstage-cli package build",
44+
"clean": "backstage-cli package clean",
45+
"lint": "backstage-cli package lint",
46+
"lint:check": "backstage-cli package lint",
47+
"lint:fix": "backstage-cli package lint --fix",
48+
"test": "backstage-cli package test --passWithNoTests --coverage",
49+
"tsc": "tsc",
50+
"start": "backstage-cli package start",
51+
"prepack": "backstage-cli package prepack",
52+
"postpack": "backstage-cli package postpack"
53+
},
54+
"files": [
55+
"dist"
56+
],
57+
"repository": {
58+
"type": "git",
59+
"url": "git+https://github.com/redhat-developer/rhdh-plugins.git",
60+
"directory": "workspaces/boost/plugins/boost-backend-module-kagenti"
61+
},
62+
"homepage": "https://red.ht/rhdh",
63+
"bugs": "https://github.com/redhat-developer/rhdh-plugins/issues"
64+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## API Report File for "@red-hat-developer-hub/backstage-plugin-boost-backend-module-kagenti"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
import { BackendFeature } from '@backstage/backend-plugin-api';
7+
8+
// @public
9+
const boostModuleKagenti: BackendFeature;
10+
export default boostModuleKagenti;
11+
```
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Kagenti provider module for the boost AI platform.
19+
*
20+
* @packageDocumentation
21+
*/
22+
23+
export { boostModuleKagenti as default } from './module';
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
coreServices,
19+
createBackendModule,
20+
} from '@backstage/backend-plugin-api';
21+
import { boostProviderExtensionPoint } from '@red-hat-developer-hub/backstage-plugin-boost-node';
22+
import { KagentiProviderFactory } from './provider/KagentiProviderFactory';
23+
24+
/**
25+
* Kagenti provider module for the boost plugin.
26+
*
27+
* Registers a KagentiProvider via the boost extension point,
28+
* enabling Kagenti as an AI backend via the A2A protocol. All
29+
* caches use Backstage cacheService (Decision 3).
30+
*
31+
* Install alongside the core boost plugin:
32+
*
33+
* ```ts
34+
* // packages/backend/src/index.ts
35+
* backend.add(import('@red-hat-developer-hub/backstage-plugin-boost-backend'));
36+
* backend.add(import('@red-hat-developer-hub/backstage-plugin-boost-backend-module-kagenti'));
37+
* ```
38+
*
39+
* @public
40+
*/
41+
export const boostModuleKagenti = createBackendModule({
42+
pluginId: 'boost',
43+
moduleId: 'kagenti',
44+
register(reg) {
45+
reg.registerInit({
46+
deps: {
47+
providers: boostProviderExtensionPoint,
48+
config: coreServices.rootConfig,
49+
cache: coreServices.cache,
50+
logger: coreServices.logger,
51+
},
52+
async init({ providers, config, cache, logger }) {
53+
logger.info('Initializing Kagenti provider module');
54+
55+
const factory = new KagentiProviderFactory({
56+
config,
57+
cache,
58+
logger,
59+
});
60+
61+
const { provider, agentCardCache, keycloakTokenCache, sessionMap } =
62+
factory.create();
63+
64+
// Register the provider with the boost plugin
65+
providers.registerProvider(provider);
66+
67+
logger.info(
68+
`Kagenti provider registered (id: ${provider.descriptor.id})`,
69+
);
70+
logger.info(
71+
`Caches initialized: agentCard (5m TTL), keycloakToken (dynamic TTL), ` +
72+
`sessionMap (24h TTL)`,
73+
);
74+
75+
// Log cache readiness (these are available for future route handlers)
76+
logger.debug(
77+
`AgentCardCache: ready, KeycloakTokenCache: ready, SessionMap: ready`,
78+
);
79+
80+
// Suppress unused-variable warnings — these caches are initialized
81+
// and available for provider-internal operations. Future iterations
82+
// will wire them into route handlers and orchestration flows.
83+
void agentCardCache;
84+
void keycloakTokenCache;
85+
void sessionMap;
86+
},
87+
});
88+
},
89+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type {
18+
CacheService,
19+
LoggerService,
20+
} from '@backstage/backend-plugin-api';
21+
import { AgentCardCache } from './AgentCardCache';
22+
import type { AgentCard } from '../types';
23+
24+
function createMockLogger(): LoggerService {
25+
return {
26+
info: jest.fn(),
27+
warn: jest.fn(),
28+
error: jest.fn(),
29+
debug: jest.fn(),
30+
child: jest.fn().mockReturnThis(),
31+
};
32+
}
33+
34+
function createMockCache(): CacheService {
35+
const store = new Map<string, unknown>();
36+
const cache: CacheService = {
37+
get: jest.fn(async (key: string) => store.get(key)) as CacheService['get'],
38+
set: jest.fn(async (key: string, value: unknown) => {
39+
store.set(key, value);
40+
}),
41+
delete: jest.fn(async (key: string) => {
42+
store.delete(key);
43+
}),
44+
withOptions: jest.fn().mockReturnThis(),
45+
};
46+
return cache;
47+
}
48+
49+
describe('AgentCardCache', () => {
50+
let cache: CacheService;
51+
let agentCardCache: AgentCardCache;
52+
53+
beforeEach(() => {
54+
cache = createMockCache();
55+
agentCardCache = new AgentCardCache({
56+
cache,
57+
logger: createMockLogger(),
58+
});
59+
});
60+
61+
it('uses cacheService withOptions with 5m TTL', () => {
62+
expect(cache.withOptions).toHaveBeenCalledWith({
63+
defaultTtl: { minutes: 5 },
64+
});
65+
});
66+
67+
it('stores and retrieves agent cards', async () => {
68+
const cards: AgentCard[] = [
69+
{ id: 'agent-1', name: 'Agent One', url: 'http://agent-1:8080' },
70+
{ id: 'agent-2', name: 'Agent Two', url: 'http://agent-2:8080' },
71+
];
72+
73+
await agentCardCache.set(cards);
74+
const result = await agentCardCache.get();
75+
expect(result).toEqual(cards);
76+
});
77+
78+
it('returns undefined when no cards are cached', async () => {
79+
const result = await agentCardCache.get();
80+
expect(result).toBeUndefined();
81+
});
82+
83+
it('invalidates cached cards', async () => {
84+
const cards: AgentCard[] = [
85+
{ id: 'agent-1', name: 'Agent One', url: 'http://agent-1:8080' },
86+
];
87+
88+
await agentCardCache.set(cards);
89+
await agentCardCache.invalidate();
90+
const result = await agentCardCache.get();
91+
expect(result).toBeUndefined();
92+
});
93+
94+
it('handles pre-parsed array returned by cache backend', async () => {
95+
const cards: AgentCard[] = [
96+
{ id: 'agent-1', name: 'Agent One', url: 'http://agent-1:8080' },
97+
];
98+
(cache.get as jest.Mock).mockResolvedValueOnce(cards);
99+
100+
const result = await agentCardCache.get();
101+
expect(result).toEqual(cards);
102+
});
103+
});

0 commit comments

Comments
 (0)