-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Add signal graph AI debugging tool #67985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dgp1130
wants to merge
4
commits into
angular:main
Choose a base branch
from
dgp1130:signal-graph-tool
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
415854e
refactor(core): define tool interfaces for AI agents
dgp1130 9bf0902
refactor(core): implement Angular signal graph tool
dgp1130 059c706
refactor(core): add `registerAiTools` function
dgp1130 52e2f79
feat(core): register AI runtime debugging tools
dgp1130 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| export {registerAiTools} from './registration'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| import {signalGraphTool} from './signal_graph'; | ||
| import {DevtoolsToolDiscoveryEvent} from './tool_definitions'; | ||
|
|
||
| /** | ||
| * Registers Angular AI tools with Chrome DevTools. | ||
| * | ||
| * This function listens for the `devtoolstooldiscovery` event and responds with | ||
| * the available Angular-specific tools. | ||
| * | ||
| * @returns A callback function to unregister the tools. | ||
| */ | ||
| export function registerAiTools(): () => void { | ||
| // No-op in non-browser environments. | ||
| if (typeof window === 'undefined') return () => {}; | ||
|
|
||
| function listener(inputEvent: Event): void { | ||
| const event = inputEvent as DevtoolsToolDiscoveryEvent; | ||
| event.respondWith({ | ||
| name: 'Angular', | ||
| tools: [signalGraphTool], | ||
| }); | ||
| } | ||
|
|
||
| window.addEventListener('devtoolstooldiscovery', listener); | ||
| return () => { | ||
| window.removeEventListener('devtoolstooldiscovery', listener); | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| 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'; | ||
|
|
||
| // Omit `debuggableFn` and `id` from returned signal graph to AI agent. | ||
| type AiSignalGraph = Omit<DebugSignalGraph, 'nodes'> & { | ||
| nodes: Array<Omit<DebugSignalGraph['nodes'][number], 'debuggableFn' | 'id'>>; | ||
| }; | ||
|
|
||
| /** | ||
| * Tool that exposes Angular's signal dependency graph to AI agents. | ||
| */ | ||
| export const signalGraphTool: ToolDefinition<{target: HTMLElement}, 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. | ||
|
|
||
| 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 | ||
| 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. | ||
|
|
||
| Returns: | ||
| - \`nodes\`: An array of reactive nodes discovered in the context. Each node contains: | ||
| - \`kind\`: The type of reactive node ('signal', 'computed', 'effect', or 'template' | ||
| for component template effects). | ||
| - \`value\`: The current evaluated value of the node (if applicable). | ||
| - \`label\`: The symbol name of the associated signal if available (ex. | ||
| \`const foo = signal(0);\` has \`label: 'foo'\`). | ||
| - \`epoch\`: The internal version number of the node's value. | ||
| - \`edges\`: An array of dependency links representing which nodes read from which other | ||
| nodes. | ||
| - \`consumer\`: The index in the \`nodes\` array of the node that depends on the value. | ||
| - \`producer\`: The index in the \`nodes\` array of the node that provides the value. | ||
|
|
||
| Example: An edge with \`{consumer: 2, producer: 0}\` means that \`nodes[2]\` (e.g. an | ||
| \`effect\`) reads the value of \`nodes[0]\` (e.g. a \`signal\`). | ||
| `.trim(), | ||
| inputSchema: { | ||
| type: 'object', | ||
| properties: { | ||
| target: { | ||
| type: 'object', | ||
| description: 'The element to get the signal graph for.', | ||
| 'x-mcp-type': 'HTMLElement', | ||
| }, | ||
| }, | ||
| required: ['target'], | ||
| }, | ||
| 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.'); | ||
| } | ||
|
|
||
| const graph = getSignalGraph(injector); | ||
| return { | ||
| // Filter out unneeded data. | ||
| nodes: graph.nodes.map(({id, debuggableFn, ...node}) => node), | ||
| edges: graph.edges, | ||
| }; | ||
| }, | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| /** | ||
| * Definition of an AI tool that can be exposed to Chrome DevTools. | ||
| */ | ||
| export interface ToolDefinition<T = any, R = unknown> { | ||
| /** Name of the tool, should be namespaced (e.g., 'angular:di_graph'). */ | ||
| name: string; | ||
|
|
||
| /** Human-readable description of what the tool does. */ | ||
| description: string; | ||
|
|
||
| /** JSON schema for the tool's input arguments. */ | ||
| inputSchema: Record<string, unknown>; | ||
|
|
||
| /** Function that executes the tool. */ | ||
| execute: (args: T) => R | Promise<R>; | ||
| } | ||
|
|
||
| /** | ||
| * A group of related AI tools. | ||
| */ | ||
| export interface ToolGroup { | ||
| /** Name of the tool group. */ | ||
| name: string; | ||
|
|
||
| /** List of tools in this group. */ | ||
| tools: ToolDefinition[]; | ||
| } | ||
|
|
||
| /** | ||
| * Event dispatched by Chrome DevTools to discover tools in the page. | ||
| */ | ||
| export interface DevtoolsToolDiscoveryEvent extends CustomEvent { | ||
| /** Callback to register tools with DevTools. */ | ||
| respondWith(toolGroup: ToolGroup): void; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| import {signalGraphTool} from '../../../src/debug/ai/signal_graph'; | ||
| import {registerAiTools} from '../../../src/debug/ai'; | ||
| import {DevtoolsToolDiscoveryEvent} from '../../../src/debug/ai/tool_definitions'; | ||
|
|
||
| describe('registration', () => { | ||
| describe('registerAiTools', () => { | ||
| it('should register the tools', () => { | ||
| const unregister = registerAiTools(); | ||
| try { | ||
| // Verify Angular responds to the event. | ||
| const event = new CustomEvent('devtoolstooldiscovery') as DevtoolsToolDiscoveryEvent; | ||
| event.respondWith = jasmine.createSpy('respondWith'); | ||
| window.dispatchEvent(event); | ||
| expect(event.respondWith).toHaveBeenCalledOnceWith({ | ||
| name: 'Angular', | ||
|
|
||
| // Just check one tool. | ||
| tools: jasmine.arrayContaining([signalGraphTool]), | ||
| }); | ||
| } finally { | ||
| unregister(); | ||
| } | ||
|
|
||
| // After unregistering, Angular should not react to the event. | ||
| const event = new CustomEvent('devtoolstooldiscovery') as DevtoolsToolDiscoveryEvent; | ||
| event.respondWith = jasmine.createSpy('respondWith'); | ||
| window.dispatchEvent(event); | ||
| expect(event.respondWith).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| /** | ||
| * @license | ||
| * Copyright Google LLC All Rights Reserved. | ||
| * | ||
| * Use of this source code is governed by an MIT-style license that can be | ||
| * found in the LICENSE file at https://angular.dev/license | ||
| */ | ||
|
|
||
| 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'; | ||
|
|
||
| describe('signalGraphTool', () => { | ||
| beforeEach(() => { | ||
| setupFrameworkInjectorProfiler(); | ||
| }); | ||
|
|
||
| it('should discover signal graph from targeted element', async () => { | ||
| @Component({ | ||
| selector: 'signal-graph-test-root', | ||
| template: '<div>Signals test: {{ double() }}</div>', | ||
| standalone: true, | ||
| }) | ||
| class SignalGraphTestComponent { | ||
| private readonly count = signal(1, {debugName: 'count'}); | ||
| protected readonly double = computed(() => this.count() * 2, {debugName: 'double'}); | ||
|
|
||
| constructor() { | ||
| effect( | ||
| () => { | ||
| this.double(); | ||
| }, | ||
| {debugName: 'consumer'}, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| const fixture = TestBed.createComponent(SignalGraphTestComponent); | ||
| await fixture.whenStable(); | ||
|
|
||
| const rootElement = fixture.nativeElement; | ||
|
|
||
| const result = await signalGraphTool.execute({target: rootElement}); | ||
|
|
||
| expect(result.nodes).toEqual( | ||
| jasmine.arrayWithExactContents([ | ||
| jasmine.objectContaining({kind: 'signal', label: 'count', value: 1}), | ||
| jasmine.objectContaining({kind: 'computed', label: 'double', value: 2}), | ||
| jasmine.objectContaining({kind: 'effect', label: 'consumer'}), | ||
| jasmine.objectContaining({kind: 'template'}), | ||
| ]), | ||
| ); | ||
|
|
||
| const countIndex = result.nodes.findIndex((n) => n.label === 'count'); | ||
| const doubleIndex = result.nodes.findIndex((n) => n.label === 'double'); | ||
| const consumerIndex = result.nodes.findIndex((n) => n.label === 'consumer'); | ||
| const templateIndex = result.nodes.findIndex((n) => n.kind === 'template'); | ||
|
|
||
| expect(result.edges).toEqual( | ||
| jasmine.arrayWithExactContents([ | ||
| {consumer: consumerIndex, producer: doubleIndex}, | ||
| {consumer: doubleIndex, producer: countIndex}, | ||
| {consumer: templateIndex, producer: doubleIndex}, | ||
| ]), | ||
| ); | ||
| }); | ||
|
|
||
| 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 target is not an Angular component', async () => { | ||
| await expectAsync( | ||
| signalGraphTool.execute({target: document.createElement('div')}), | ||
| ).toBeRejectedWithError(/not the host element of an Angular component/); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.