Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 14 additions & 18 deletions packages/core/src/debug/ai/signal_graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {Injector} from '../../di/injector';
import {NullInjector} from '../../di/null_injector';
import {getInjector} from '../../render3/util/discovery_utils';
import {DebugSignalGraph, getSignalGraph} from '../../render3/util/signal_debug';
import {ToolDefinition} from './tool_definitions';

Expand All @@ -19,20 +19,19 @@ type AiSignalGraph = Omit<DebugSignalGraph, 'nodes'> & {
/**
* Tool that exposes Angular's signal dependency graph to AI agents.
*/
export const signalGraphTool: ToolDefinition<{target: HTMLElement}, AiSignalGraph> = {
export const signalGraphTool: ToolDefinition<{injector: Injector}, AiSignalGraph> = {
name: 'angular:signal_graph',
// tslint:disable-next-line:no-toplevel-property-access
description: `
Exposes the Angular signal dependency graph for a given DOM element.
Exposes the Angular signal dependency graph for a given Injector.

This tool extracts the reactive dependency graph (signals, computeds, and effects) that
are transitive dependencies of the effects of that element. It will include signals
are transitive dependencies of the effects of that injector. It will include signals
authored in other components/services and depended upon by the target component, but
will *not* include signals only used in descendant components effects.

Params:
- \`target\`: The element to get the signal graph for. Must be the host element of an
Angular component.
- \`injector\`: The Injector to get the signal graph for.

Returns:
- \`nodes\`: An array of reactive nodes discovered in the context. Each node contains:
Expand All @@ -53,22 +52,19 @@ Example: An edge with \`{consumer: 2, producer: 0}\` means that \`nodes[2]\` (e.
inputSchema: {
type: 'object',
properties: {
target: {
injector: {
type: 'object',
description: 'The element to get the signal graph for.',
'x-mcp-type': 'HTMLElement',
description: 'The Injector to get the signal graph for.',
'x-mcp-type': 'Injector',
},
},
required: ['target'],
required: ['injector'],
},
execute: async ({target}: {target: HTMLElement}) => {
if (!(target instanceof HTMLElement)) {
throw new Error('Invalid input: "target" must be an HTMLElement.');
}

const injector = getInjector(target);
if (injector instanceof NullInjector) {
throw new Error('Invalid input: "target" is not the host element of an Angular component.');
execute: async ({injector}: {injector: Injector}) => {
if (!injector || injector instanceof NullInjector) {
throw new Error(
'Invalid input: "injector" is undefined, null, or an instance of NullInjector',
);
}

const graph = getSignalGraph(injector);
Expand Down
27 changes: 18 additions & 9 deletions packages/core/test/debug/ai/signal_graph_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {Component, computed, effect, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {signalGraphTool} from '../../../src/debug/ai/signal_graph';
import {setupFrameworkInjectorProfiler} from '../../../src/render3/debug/framework_injector_profiler';
import {getInjector} from '../../../src/render3/util/discovery_utils';
import {Injector} from '../../../src/di/injector';

describe('signalGraphTool', () => {
beforeEach(() => {
setupFrameworkInjectorProfiler();
});

it('should discover signal graph from targeted element', async () => {
it('should discover signal graph from targeted injector', async () => {
@Component({
selector: 'signal-graph-test-root',
template: '<div>Signals test: {{ double() }}</div>',
Expand All @@ -40,8 +42,9 @@ describe('signalGraphTool', () => {
await fixture.whenStable();

const rootElement = fixture.nativeElement;
const injector = getInjector(rootElement);

const result = await signalGraphTool.execute({target: rootElement});
const result = await signalGraphTool.execute({injector});

expect(result.nodes).toEqual(
jasmine.arrayWithExactContents([
Expand All @@ -66,15 +69,21 @@ describe('signalGraphTool', () => {
);
});

it('should throw an error if target is not an HTMLElement', async () => {
await expectAsync(
signalGraphTool.execute({target: {} as unknown as HTMLElement}),
).toBeRejectedWithError(/must be an HTMLElement/);
it('should throw an error if injector is null', async () => {
await expectAsync(signalGraphTool.execute({injector: null!})).toBeRejectedWithError(
/undefined, null, or an instance of NullInjector/,
);
});

it('should throw an error if injector is undefined', async () => {
await expectAsync(signalGraphTool.execute({injector: undefined!})).toBeRejectedWithError(
/undefined, null, or an instance of NullInjector/,
);
});

it('should throw an error if target is not an Angular component', async () => {
it('should throw an error if injector is a NullInjector', async () => {
await expectAsync(
signalGraphTool.execute({target: document.createElement('div')}),
).toBeRejectedWithError(/not the host element of an Angular component/);
signalGraphTool.execute({injector: getInjector(document.createElement('div'))}),
).toBeRejectedWithError(/undefined, null, or an instance of NullInjector/);
});
});
Loading