-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(nodes): add Steadwing root-cause-analysis as an agent tool #1249
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # ============================================================================= | ||
| # RocketRide Engine | ||
| # ============================================================================= | ||
| # MIT License | ||
| # Copyright (c) 2026 Aparavi Software AG | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in | ||
| # all copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| # ============================================================================= | ||
|
|
||
| """ | ||
| Steadwing tool node - global (shared) state. | ||
|
|
||
| Reads the Steadwing API key from the node config (or the ROCKETRIDE_STEADWING_KEY | ||
| env var). The root-cause-analysis tool itself lives on IInstance via @tool_function. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
|
|
||
| from ai.common.config import Config | ||
| from rocketlib import IGlobalBase, OPEN_MODE, error, warning | ||
|
|
||
| # Pipeline env vars must be ROCKETRIDE_-prefixed (only those are substituted, and | ||
| # the node-test framework maps ROCKETRIDE_<PROVIDER>_<ATTR> -> config). | ||
| STEADWING_API_KEY_ENV = 'ROCKETRIDE_STEADWING_KEY' | ||
|
|
||
|
|
||
| class IGlobal(IGlobalBase): | ||
| """Global state for tool_steadwing.""" | ||
|
|
||
| apikey: str = '' | ||
|
|
||
| def beginGlobal(self) -> None: | ||
| if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG: | ||
| return | ||
|
|
||
| cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) | ||
|
|
||
| apikey = str(cfg.get('apikey') or '').strip() or os.environ.get(STEADWING_API_KEY_ENV, '').strip() | ||
|
|
||
| if not apikey: | ||
| error(f'tool_steadwing: apikey is required — set it in node config or the {STEADWING_API_KEY_ENV} env var') | ||
| raise ValueError('tool_steadwing: apikey is required') | ||
|
|
||
| self.apikey = apikey | ||
|
|
||
| def validateConfig(self) -> None: | ||
| try: | ||
| cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) | ||
| apikey = str(cfg.get('apikey') or '').strip() or os.environ.get(STEADWING_API_KEY_ENV, '').strip() | ||
| if not apikey: | ||
| warning('apikey is required') | ||
| except Exception as e: | ||
|
Comment on lines
+64
to
+70
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 67 reads 🤖 Prompt for AI Agents |
||
| warning(str(e)) | ||
|
|
||
| def endGlobal(self) -> None: | ||
| self.apikey = '' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| # ============================================================================= | ||
| # RocketRide Engine | ||
| # ============================================================================= | ||
| # MIT License | ||
| # Copyright (c) 2026 Aparavi Software AG | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in | ||
| # all copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| # ============================================================================= | ||
|
|
||
| """ | ||
| Steadwing tool node instance. | ||
|
|
||
| Exposes ``run_rca`` as a @tool_function: given an error / stack trace (and optional | ||
| source files), it calls Steadwing's root-cause-analysis API and returns the URL of | ||
| the Steadwing investigation where the cross-tool RCA runs. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, Dict, List | ||
|
|
||
| from rocketlib import IInstanceBase, tool_function, warning | ||
|
|
||
| from ai.common.utils import normalize_tool_input, post_with_retry | ||
|
|
||
| from .IGlobal import IGlobal | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Steadwing API configuration | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| STEADWING_API_BASE = 'https://api.steadwing.com' | ||
| STEADWING_ANALYZE_ENDPOINT = f'{STEADWING_API_BASE}/api/mcp/analyze' | ||
| STEADWING_REQUEST_TIMEOUT = 60 # seconds | ||
| MAX_FILES = 20 | ||
|
|
||
|
|
||
| class IInstance(IInstanceBase): | ||
| """Node instance exposing Steadwing root-cause analysis as an agent tool.""" | ||
|
|
||
| IGlobal: IGlobal | ||
|
|
||
| @tool_function( | ||
| input_schema={ | ||
| 'type': 'object', | ||
| 'required': ['error'], | ||
| 'properties': { | ||
| 'error': { | ||
| 'type': 'string', | ||
| 'description': ( | ||
| 'The complete error message, stack trace, or incident description to ' | ||
| 'analyze. Include as much context as possible — error type, message, ' | ||
| 'stack trace, line numbers, and what was happening when it occurred.' | ||
| ), | ||
| }, | ||
| 'files': { | ||
| 'type': 'array', | ||
| 'description': ( | ||
| 'Optional source files that give the analysis context (max 20). Start with ' | ||
| 'the file named in the stack trace, then its direct imports and any relevant ' | ||
| 'configuration.' | ||
| ), | ||
| 'items': { | ||
| 'type': 'object', | ||
| 'required': ['name', 'content'], | ||
| 'properties': { | ||
| 'name': { | ||
| 'type': 'string', | ||
| 'description': 'Relative path from the project root, e.g. "src/app.js".', | ||
| }, | ||
| 'content': { | ||
| 'type': 'string', | ||
| 'description': 'Complete, unmodified file contents.', | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| output_schema={ | ||
| 'type': 'object', | ||
| 'properties': { | ||
| 'success': {'type': 'boolean'}, | ||
| 'incident_url': { | ||
| 'type': 'string', | ||
| 'description': 'URL of the Steadwing investigation where the RCA runs.', | ||
| }, | ||
| 'message': {'type': 'string'}, | ||
| }, | ||
| }, | ||
| description=( | ||
| 'Run AI-powered root-cause analysis on a production error or incident using Steadwing. ' | ||
| 'Provide the full error message / stack trace as `error`, and the relevant source `files` ' | ||
| 'when you have them. Steadwing correlates logs, metrics, traces, and code across the stack ' | ||
| 'and returns the URL of an investigation that then runs in the background. Call this ONCE ' | ||
| 'per incident — the returned URL is the deliverable; do not call it again or wait for more.' | ||
| ), | ||
| ) | ||
| def run_rca(self, args): | ||
| """Open a Steadwing root-cause analysis for an error / incident.""" | ||
| args = normalize_tool_input(args, tool_name='steadwing') | ||
|
|
||
| error_text = (args.get('error') or '').strip() | ||
| if not error_text: | ||
| raise ValueError('error is required and must be a non-empty string') | ||
|
|
||
| payload: Dict[str, Any] = {'error_log': error_text} | ||
| files = _normalize_files(args.get('files')) | ||
| if files: | ||
| payload['files'] = files | ||
|
|
||
| headers = { | ||
| 'accept': 'application/json', | ||
| 'content-type': 'application/json', | ||
| 'X-API-Key': self.IGlobal.apikey, | ||
| } | ||
|
|
||
| resp = post_with_retry( | ||
| STEADWING_ANALYZE_ENDPOINT, headers=headers, json=payload, timeout=STEADWING_REQUEST_TIMEOUT | ||
| ) | ||
|
Comment on lines
+134
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Locate post_with_retry definition(s):"
rg -n --type=py "def post_with_retry\("
echo
echo "Show retry logic around post_with_retry:"
rg -n -A80 -B20 --type=py "def post_with_retry\("
echo
echo "Find POST call sites using post_with_retry (including Steadwing):"
rg -n --type=py "post_with_retry\("
echo
echo "Inspect Steadwing call site context:"
rg -n -A20 -B10 --type=py "STEADWING_ANALYZE_ENDPOINT|run_rca|post_with_retry"Repository: rocketride-org/rocketride-server Length of output: 50388 Steadwing analyze POST can be retried—needs idempotency guard
At resp = post_with_retry(
STEADWING_ANALYZE_ENDPOINT, headers=headers, json=payload, timeout=STEADWING_REQUEST_TIMEOUT
)Please confirm the Steadwing 🤖 Prompt for AI Agents |
||
| try: | ||
| body = resp.json() | ||
| except ValueError as exc: | ||
| # Malformed / non-JSON body. Log the status only — never the body, | ||
| # which can echo the submitted error context — and re-raise. | ||
| status = getattr(resp, 'status_code', None) | ||
| warning(f'Steadwing API returned a non-JSON response body: status={status}') | ||
| raise RuntimeError('Steadwing returned a non-JSON response body') from exc | ||
|
|
||
| if not isinstance(body, dict): | ||
| raise RuntimeError(f'Steadwing returned an unexpected payload type: {type(body).__name__}') | ||
|
|
||
| api_error = body.get('error') | ||
| if isinstance(api_error, dict): | ||
| msg = api_error.get('message') or api_error.get('detail') or api_error.get('code') or 'unknown error' | ||
| raise RuntimeError(f'Steadwing API error: {msg}') | ||
| if api_error: | ||
| raise RuntimeError(f'Steadwing API error: {api_error}') | ||
|
|
||
| return _shape_result(body) | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Helpers | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _normalize_files(files: Any) -> List[Dict[str, str]]: | ||
| """Coerce the optional ``files`` argument into a clean ``[{name, content}]`` list (max 20).""" | ||
| if not files: | ||
| return [] | ||
| if not isinstance(files, list): | ||
| raise ValueError('files must be an array of {name, content} objects') | ||
|
|
||
| out: List[Dict[str, str]] = [] | ||
| for item in files[:MAX_FILES]: | ||
| if not isinstance(item, dict): | ||
| continue | ||
| name = str(item.get('name') or '').strip() | ||
| content = item.get('content') | ||
| if not name or not isinstance(content, str): | ||
| continue | ||
| out.append({'name': name, 'content': content}) | ||
| return out | ||
|
|
||
|
|
||
| def _shape_result(body: Dict[str, Any]) -> Dict[str, Any]: | ||
| """Extract the Steadwing investigation URL from an ``analyze`` response. | ||
|
|
||
| The endpoint returns the URL either at the root or nested under ``data`` | ||
| (``{"data": {"incident_url": ...}}``); accept either, plus a few key spellings. | ||
| """ | ||
| data = body.get('data') if isinstance(body.get('data'), dict) else {} | ||
| incident_url = ( | ||
| data.get('incident_url') | ||
| or data.get('incidentUrl') | ||
| or data.get('url') | ||
| or body.get('incident_url') | ||
| or body.get('incidentUrl') | ||
| or body.get('url') | ||
| or '' | ||
| ) | ||
| incident_url = str(incident_url or '').strip() | ||
| if not incident_url: | ||
| raise RuntimeError('Steadwing response did not include an investigation URL') | ||
|
|
||
| return { | ||
| 'success': True, | ||
| 'incident_url': incident_url, | ||
| 'message': f'Root-cause analysis started. Track the investigation at {incident_url}', | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # ============================================================================= | ||
| # RocketRide Engine | ||
| # ============================================================================= | ||
| # MIT License | ||
| # Copyright (c) 2026 Aparavi Software AG | ||
| # | ||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| # of this software and associated documentation files (the "Software"), to deal | ||
| # in the Software without restriction, including without limitation the rights | ||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| # copies of the Software, and to permit persons to whom the Software is | ||
| # furnished to do so, subject to the following conditions: | ||
| # | ||
| # The above copyright notice and this permission notice shall be included in | ||
| # all copies or substantial portions of the Software. | ||
| # | ||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| # SOFTWARE. | ||
| # ============================================================================= | ||
|
|
||
| from .IGlobal import IGlobal | ||
| from .IInstance import IInstance | ||
|
|
||
| __all__ = ['IGlobal', 'IInstance'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # tool_steadwing | ||
|
|
||
| Exposes [Steadwing](https://www.steadwing.com)'s AI root-cause analysis as an agent tool node. | ||
|
|
||
| ## What it does | ||
|
|
||
| Agents invoke this node via the tool invoke channel. Given a production error or stack | ||
| trace (and, optionally, the relevant source files), `run_rca` opens a Steadwing | ||
| investigation that correlates logs, metrics, traces, and code across the stack, and | ||
| returns the investigation URL. | ||
|
|
||
| Because `lanes` is empty (`{}`), this node has no pipeline input/output lanes — it is | ||
| consumed exclusively by agent runtimes through the `invoke` capability. | ||
|
|
||
| `run_rca` is **asynchronous**: it returns the investigation URL immediately and the | ||
| analysis runs in the background on the Steadwing platform. Call it once per incident — | ||
| the URL is the deliverable. | ||
|
|
||
| ## Setup | ||
|
|
||
| Set your Steadwing API key (from `app.steadwing.com/organization`) via the node config | ||
| field **API Key** or the environment variable: | ||
|
|
||
| ```bash | ||
| ROCKETRIDE_STEADWING_KEY=st_... | ||
| ``` | ||
|
|
||
| ## Tool — `run_rca` | ||
|
|
||
| | Parameter | Required | Description | | ||
| | --------- | -------- | ----------------------------------------------------------------------------------- | | ||
| | `error` | yes | The full error message, stack trace, or incident description to analyze. | | ||
| | `files` | no | Up to 20 source files (`{ name, content }`) for context — start with the file named in the stack trace. | | ||
|
|
||
| Returns `{ success, incident_url, message }`. Backed by | ||
| `POST https://api.steadwing.com/api/mcp/analyze` (`X-API-Key`). | ||
|
|
||
| ## Config fields | ||
|
|
||
| | Field | Default | Description | | ||
| | ------- | --------- | --------------------------------------------------------------------------- | | ||
| | API Key | *(empty)* | Steadwing API key. Env fallback `ROCKETRIDE_STEADWING_KEY`. Encrypted at rest. | | ||
|
|
||
| ## Status — V0 | ||
|
|
||
| Intentional first cut: a minimal `run_rca` wrapper. Planned next (notably for power | ||
| users): return the finished analysis (not just the tracking URL); richer `files` | ||
| ergonomics; idempotency / guardrails so agent loops don't burn metered RCAs; a | ||
| Steadwing-incident → pipeline trigger node for autonomous on-call; power-user config | ||
| (analysis depth, integrations, output shaping). | ||
|
|
||
| ## Reference | ||
|
|
||
| <!-- ROCKETRIDE:GENERATED:PARAMS START --> | ||
| <!-- Generated by nodes:docs-generate. Do not edit by hand. --> | ||
|
|
||
| | Property | Value | | ||
| | --- | --- | | ||
| | Class type | tool | | ||
| | Capabilities | invoke, experimental | | ||
| | Protocol | `tool_steadwing://` | | ||
|
|
||
| **Profiles** | ||
|
|
||
| | Profile | Title | Model | | ||
| | --- | --- | --- | | ||
| | `default` | Steadwing RCA | | | ||
|
|
||
| **Configuration sections** | ||
|
|
||
| | Section | Fields | | ||
| | --- | --- | | ||
| | Steadwing RCA | `type`, `tool_steadwing.apikey` | | ||
| <!-- ROCKETRIDE:GENERATED:PARAMS END --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a blank line after the heading to satisfy markdownlint.
Line 12 should be followed by an empty line before the list at Line 13 (MD022).
Suggested diff
#### New Nodes + - **Steadwing RCA** — agent tool exposing Steadwing's AI root-cause analysis (`run_rca`). Given an error or stack trace (and optional source files), it opens a Steadwing investigation that correlates logs, metrics, traces, and code across the stack, and returns the investigation URL. REST-backed (`POST /api/mcp/analyze`, `X-API-Key`) with a secure API key (env fallback `ROCKETRIDE_STEADWING_KEY`). Experimental V0. (`#1248`)📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 12-12: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🤖 Prompt for AI Agents
Source: Linters/SAST tools