kestrel-feature-github 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/.gitignore +1 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/PKG-INFO +1 -1
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/feature.py +140 -47
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/pyproject.toml +1 -1
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/tests/test_github_feature.py +32 -2
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/.github/workflows/ci.yml +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/.github/workflows/publish.yml +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/AGENTS.md +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/LICENSE +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/README.md +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/SKILL.md +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/__init__.py +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/ast_analyzer.py +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/cache.py +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/client.py +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/models.py +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/tests/__init__.py +0 -0
- {kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/tests/conftest.py +0 -0
{kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/feature.py
RENAMED
|
@@ -7,6 +7,7 @@ import yaml
|
|
|
7
7
|
|
|
8
8
|
from kestrel_sdk.features.base import Feature, tool
|
|
9
9
|
from kestrel_sdk.tools.base import ToolCategory
|
|
10
|
+
from kestrel_sdk.tools.result import ToolResult
|
|
10
11
|
|
|
11
12
|
from .ast_analyzer import ASTAnalyzer
|
|
12
13
|
from .cache import GitHubCache
|
|
@@ -90,7 +91,7 @@ class GitHubFeature(Feature):
|
|
|
90
91
|
repo: str,
|
|
91
92
|
path: str,
|
|
92
93
|
ref: str = "main",
|
|
93
|
-
) ->
|
|
94
|
+
) -> ToolResult:
|
|
94
95
|
"""Read a file from GitHub.
|
|
95
96
|
|
|
96
97
|
Args:
|
|
@@ -108,16 +109,22 @@ class GitHubFeature(Feature):
|
|
|
108
109
|
# Check cache first
|
|
109
110
|
cached = await self.cache.get(repo, path, ref)
|
|
110
111
|
if cached:
|
|
111
|
-
return
|
|
112
|
+
return ToolResult.ok(
|
|
113
|
+
f"# {path} (cached)\n\n{cached.content}",
|
|
114
|
+
data={"repo": repo, "path": path, "ref": ref, "cached": True},
|
|
115
|
+
)
|
|
112
116
|
|
|
113
117
|
# Fetch from GitHub
|
|
114
118
|
try:
|
|
115
119
|
content = await self.client.get_file_content(repo, path, ref)
|
|
116
120
|
# Cache it
|
|
117
121
|
await self.cache.set(content)
|
|
118
|
-
return
|
|
122
|
+
return ToolResult.ok(
|
|
123
|
+
f"# {path}\n\n{content.content}",
|
|
124
|
+
data={"repo": repo, "path": path, "ref": ref, "cached": False},
|
|
125
|
+
)
|
|
119
126
|
except GitHubClientError as e:
|
|
120
|
-
return f"Error reading {path}: {e}"
|
|
127
|
+
return ToolResult.failed(error=f"Error reading {path}: {e}")
|
|
121
128
|
|
|
122
129
|
@tool(
|
|
123
130
|
name="list_github_files",
|
|
@@ -130,7 +137,7 @@ class GitHubFeature(Feature):
|
|
|
130
137
|
path: str = "",
|
|
131
138
|
ref: str = "main",
|
|
132
139
|
recursive: bool = False,
|
|
133
|
-
) ->
|
|
140
|
+
) -> ToolResult:
|
|
134
141
|
"""List files in a directory.
|
|
135
142
|
|
|
136
143
|
Args:
|
|
@@ -165,9 +172,12 @@ class GitHubFeature(Feature):
|
|
|
165
172
|
size = f"{f.size:,}" if f.size else "?"
|
|
166
173
|
lines.append(f"\U0001f4c4 {f.path} ({size} bytes)")
|
|
167
174
|
|
|
168
|
-
return
|
|
175
|
+
return ToolResult.ok(
|
|
176
|
+
"\n".join(lines),
|
|
177
|
+
data={"repo": repo, "path": path, "ref": ref, "count": len(files)},
|
|
178
|
+
)
|
|
169
179
|
except GitHubClientError as e:
|
|
170
|
-
return f"Error listing {path}: {e}"
|
|
180
|
+
return ToolResult.failed(error=f"Error listing {path}: {e}")
|
|
171
181
|
|
|
172
182
|
@tool(
|
|
173
183
|
name="search_github_code",
|
|
@@ -181,7 +191,7 @@ class GitHubFeature(Feature):
|
|
|
181
191
|
path: Optional[str] = None,
|
|
182
192
|
extension: Optional[str] = None,
|
|
183
193
|
max_results: int = 20,
|
|
184
|
-
) ->
|
|
194
|
+
) -> ToolResult:
|
|
185
195
|
"""Search for code in GitHub.
|
|
186
196
|
|
|
187
197
|
Args:
|
|
@@ -203,7 +213,10 @@ class GitHubFeature(Feature):
|
|
|
203
213
|
)
|
|
204
214
|
|
|
205
215
|
if not results:
|
|
206
|
-
return
|
|
216
|
+
return ToolResult.ok(
|
|
217
|
+
f"No results found for: {query}",
|
|
218
|
+
data={"query": query, "count": 0},
|
|
219
|
+
)
|
|
207
220
|
|
|
208
221
|
lines = [f"# Search results for: {query}\n"]
|
|
209
222
|
|
|
@@ -217,9 +230,12 @@ class GitHubFeature(Feature):
|
|
|
217
230
|
if fragment:
|
|
218
231
|
lines.append(f"\n```\n{fragment}\n```")
|
|
219
232
|
|
|
220
|
-
return
|
|
233
|
+
return ToolResult.ok(
|
|
234
|
+
"\n".join(lines),
|
|
235
|
+
data={"query": query, "count": len(results)},
|
|
236
|
+
)
|
|
221
237
|
except GitHubClientError as e:
|
|
222
|
-
return f"Search error: {e}"
|
|
238
|
+
return ToolResult.failed(error=f"Search error: {e}")
|
|
223
239
|
|
|
224
240
|
@tool(
|
|
225
241
|
name="get_code_definition",
|
|
@@ -232,7 +248,7 @@ class GitHubFeature(Feature):
|
|
|
232
248
|
path: str,
|
|
233
249
|
name: str,
|
|
234
250
|
ref: str = "main",
|
|
235
|
-
) ->
|
|
251
|
+
) -> ToolResult:
|
|
236
252
|
"""Get a specific function or class definition.
|
|
237
253
|
|
|
238
254
|
Args:
|
|
@@ -249,7 +265,7 @@ class GitHubFeature(Feature):
|
|
|
249
265
|
ref = GITHUB_DEFAULT_BRANCH
|
|
250
266
|
|
|
251
267
|
if not path.endswith(".py"):
|
|
252
|
-
return "
|
|
268
|
+
return ToolResult.failed(error="AST analysis only supports Python files (.py)")
|
|
253
269
|
|
|
254
270
|
# Get file content
|
|
255
271
|
try:
|
|
@@ -261,7 +277,7 @@ class GitHubFeature(Feature):
|
|
|
261
277
|
await self.cache.set(file_content)
|
|
262
278
|
content = file_content.content
|
|
263
279
|
except GitHubClientError as e:
|
|
264
|
-
return f"Error reading {path}: {e}"
|
|
280
|
+
return ToolResult.failed(error=f"Error reading {path}: {e}")
|
|
265
281
|
|
|
266
282
|
# Parse and find definition
|
|
267
283
|
analyzer = ASTAnalyzer(content, path)
|
|
@@ -271,9 +287,13 @@ class GitHubFeature(Feature):
|
|
|
271
287
|
# List available definitions
|
|
272
288
|
all_defs = analyzer.get_definitions()
|
|
273
289
|
available = [d.name for d in all_defs[:20]]
|
|
274
|
-
return
|
|
290
|
+
return ToolResult.ok(
|
|
291
|
+
f"Definition '{name}' not found in {path}.\n\nAvailable: {', '.join(available)}",
|
|
292
|
+
data={"repo": repo, "path": path, "name": name, "found": False, "available": available},
|
|
293
|
+
)
|
|
275
294
|
|
|
276
|
-
return
|
|
295
|
+
return ToolResult.ok(
|
|
296
|
+
f"""# {defn.type.title()}: {defn.name}
|
|
277
297
|
|
|
278
298
|
**File:** {path}
|
|
279
299
|
**Lines:** {defn.start_line}-{defn.end_line}
|
|
@@ -285,7 +305,17 @@ class GitHubFeature(Feature):
|
|
|
285
305
|
## Source
|
|
286
306
|
```python
|
|
287
307
|
{defn.source}
|
|
288
|
-
```"""
|
|
308
|
+
```""",
|
|
309
|
+
data={
|
|
310
|
+
"repo": repo,
|
|
311
|
+
"path": path,
|
|
312
|
+
"name": defn.name,
|
|
313
|
+
"type": defn.type,
|
|
314
|
+
"start_line": defn.start_line,
|
|
315
|
+
"end_line": defn.end_line,
|
|
316
|
+
"found": True,
|
|
317
|
+
},
|
|
318
|
+
)
|
|
289
319
|
|
|
290
320
|
@tool(
|
|
291
321
|
name="list_code_definitions",
|
|
@@ -297,7 +327,7 @@ class GitHubFeature(Feature):
|
|
|
297
327
|
repo: str,
|
|
298
328
|
path: str,
|
|
299
329
|
ref: str = "main",
|
|
300
|
-
) ->
|
|
330
|
+
) -> ToolResult:
|
|
301
331
|
"""List all definitions in a Python file.
|
|
302
332
|
|
|
303
333
|
Args:
|
|
@@ -313,7 +343,7 @@ class GitHubFeature(Feature):
|
|
|
313
343
|
ref = GITHUB_DEFAULT_BRANCH
|
|
314
344
|
|
|
315
345
|
if not path.endswith(".py"):
|
|
316
|
-
return "
|
|
346
|
+
return ToolResult.failed(error="AST analysis only supports Python files (.py)")
|
|
317
347
|
|
|
318
348
|
# Get file content
|
|
319
349
|
try:
|
|
@@ -325,14 +355,17 @@ class GitHubFeature(Feature):
|
|
|
325
355
|
await self.cache.set(file_content)
|
|
326
356
|
content = file_content.content
|
|
327
357
|
except GitHubClientError as e:
|
|
328
|
-
return f"Error reading {path}: {e}"
|
|
358
|
+
return ToolResult.failed(error=f"Error reading {path}: {e}")
|
|
329
359
|
|
|
330
360
|
# Parse
|
|
331
361
|
analyzer = ASTAnalyzer(content, path)
|
|
332
362
|
definitions = analyzer.get_definitions()
|
|
333
363
|
|
|
334
364
|
if not definitions:
|
|
335
|
-
return
|
|
365
|
+
return ToolResult.ok(
|
|
366
|
+
f"No function or class definitions found in {path}",
|
|
367
|
+
data={"repo": repo, "path": path, "count": 0},
|
|
368
|
+
)
|
|
336
369
|
|
|
337
370
|
lines = [f"# Definitions in {path}\n"]
|
|
338
371
|
|
|
@@ -358,14 +391,24 @@ class GitHubFeature(Feature):
|
|
|
358
391
|
if len(methods) > 30:
|
|
359
392
|
lines.append(f" ... and {len(methods) - 30} more")
|
|
360
393
|
|
|
361
|
-
return
|
|
394
|
+
return ToolResult.ok(
|
|
395
|
+
"\n".join(lines),
|
|
396
|
+
data={
|
|
397
|
+
"repo": repo,
|
|
398
|
+
"path": path,
|
|
399
|
+
"count": len(definitions),
|
|
400
|
+
"classes": len(classes),
|
|
401
|
+
"functions": len(functions),
|
|
402
|
+
"methods": len(methods),
|
|
403
|
+
},
|
|
404
|
+
)
|
|
362
405
|
|
|
363
406
|
@tool(
|
|
364
407
|
name="get_self_repo_info",
|
|
365
408
|
description="Get information about the agent's own source repository.",
|
|
366
409
|
category=ToolCategory.DATA_ACCESS,
|
|
367
410
|
)
|
|
368
|
-
async def get_self_repo_info(self) ->
|
|
411
|
+
async def get_self_repo_info(self) -> ToolResult:
|
|
369
412
|
"""Get info about the agent's own repository.
|
|
370
413
|
|
|
371
414
|
Returns:
|
|
@@ -376,7 +419,8 @@ class GitHubFeature(Feature):
|
|
|
376
419
|
try:
|
|
377
420
|
info = await self.client.get_repo_info(repo)
|
|
378
421
|
|
|
379
|
-
return
|
|
422
|
+
return ToolResult.ok(
|
|
423
|
+
f"""# Agent Source Repository
|
|
380
424
|
|
|
381
425
|
**Repository:** {info.get('full_name')}
|
|
382
426
|
**Description:** {info.get('description', 'N/A')}
|
|
@@ -392,16 +436,24 @@ class GitHubFeature(Feature):
|
|
|
392
436
|
- Open Issues: {info.get('open_issues_count', 0)}
|
|
393
437
|
- Last Updated: {info.get('updated_at', 'unknown')}
|
|
394
438
|
|
|
395
|
-
Use `list_source_components` to see the feature components that make up this agent."""
|
|
439
|
+
Use `list_source_components` to see the feature components that make up this agent.""",
|
|
440
|
+
data={
|
|
441
|
+
"repo": info.get("full_name", repo),
|
|
442
|
+
"default_branch": info.get("default_branch"),
|
|
443
|
+
"visibility": info.get("visibility"),
|
|
444
|
+
"open_issues_count": info.get("open_issues_count"),
|
|
445
|
+
"url": info.get("html_url"),
|
|
446
|
+
},
|
|
447
|
+
)
|
|
396
448
|
except GitHubClientError as e:
|
|
397
|
-
return f"Error getting repo info: {e}"
|
|
449
|
+
return ToolResult.failed(error=f"Error getting repo info: {e}")
|
|
398
450
|
|
|
399
451
|
@tool(
|
|
400
452
|
name="list_source_components",
|
|
401
453
|
description="List all feature components in the agent's source code with their manifests.",
|
|
402
454
|
category=ToolCategory.DATA_ACCESS,
|
|
403
455
|
)
|
|
404
|
-
async def list_source_components(self, include_files: bool = False) ->
|
|
456
|
+
async def list_source_components(self, include_files: bool = False) -> ToolResult:
|
|
405
457
|
"""List all feature components.
|
|
406
458
|
|
|
407
459
|
Args:
|
|
@@ -417,7 +469,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
417
469
|
try:
|
|
418
470
|
files = await self.client.list_directory(repo, GITHUB_SELF_FEATURES_ROOT, ref)
|
|
419
471
|
except GitHubClientError as e:
|
|
420
|
-
return f"Could not access features directory: {e}"
|
|
472
|
+
return ToolResult.failed(error=f"Could not access features directory: {e}")
|
|
421
473
|
|
|
422
474
|
components = []
|
|
423
475
|
|
|
@@ -477,7 +529,10 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
477
529
|
if len(comp["files"]) > 20:
|
|
478
530
|
lines.append(f" ... and {len(comp['files']) - 20} more")
|
|
479
531
|
|
|
480
|
-
return
|
|
532
|
+
return ToolResult.ok(
|
|
533
|
+
"\n".join(lines),
|
|
534
|
+
data={"repo": repo, "component_count": len(components)},
|
|
535
|
+
)
|
|
481
536
|
|
|
482
537
|
@tool(
|
|
483
538
|
name="get_component_source",
|
|
@@ -488,7 +543,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
488
543
|
self,
|
|
489
544
|
component: str,
|
|
490
545
|
include_content: bool = False,
|
|
491
|
-
) ->
|
|
546
|
+
) -> ToolResult:
|
|
492
547
|
"""Get source files for a component.
|
|
493
548
|
|
|
494
549
|
Args:
|
|
@@ -511,10 +566,13 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
511
566
|
if f.path.startswith(component_path + "/") and f.is_file()
|
|
512
567
|
]
|
|
513
568
|
except GitHubClientError as e:
|
|
514
|
-
return f"Could not access component '{component}': {e}"
|
|
569
|
+
return ToolResult.failed(error=f"Could not access component '{component}': {e}")
|
|
515
570
|
|
|
516
571
|
if not comp_files:
|
|
517
|
-
return
|
|
572
|
+
return ToolResult.ok(
|
|
573
|
+
f"Component '{component}' not found or has no files",
|
|
574
|
+
data={"component": component, "file_count": 0},
|
|
575
|
+
)
|
|
518
576
|
|
|
519
577
|
lines = [f"# Component: {component}\n"]
|
|
520
578
|
lines.append(f"**Path:** {component_path}")
|
|
@@ -546,7 +604,10 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
546
604
|
else:
|
|
547
605
|
lines.append(f"*Size: {f.size:,} bytes*")
|
|
548
606
|
|
|
549
|
-
return
|
|
607
|
+
return ToolResult.ok(
|
|
608
|
+
"\n".join(lines),
|
|
609
|
+
data={"component": component, "path": component_path, "file_count": len(comp_files)},
|
|
610
|
+
)
|
|
550
611
|
|
|
551
612
|
@tool(
|
|
552
613
|
name="invalidate_github_cache",
|
|
@@ -557,7 +618,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
557
618
|
self,
|
|
558
619
|
repo: str,
|
|
559
620
|
path: Optional[str] = None,
|
|
560
|
-
) ->
|
|
621
|
+
) -> ToolResult:
|
|
561
622
|
"""Invalidate cache entries.
|
|
562
623
|
|
|
563
624
|
Args:
|
|
@@ -572,8 +633,14 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
572
633
|
await self.cache.invalidate(repo, path=path)
|
|
573
634
|
|
|
574
635
|
if path:
|
|
575
|
-
return
|
|
576
|
-
|
|
636
|
+
return ToolResult.ok(
|
|
637
|
+
f"Invalidated cache for {repo}:{path}",
|
|
638
|
+
data={"repo": repo, "path": path},
|
|
639
|
+
)
|
|
640
|
+
return ToolResult.ok(
|
|
641
|
+
f"Invalidated all cache for {repo}",
|
|
642
|
+
data={"repo": repo, "path": None},
|
|
643
|
+
)
|
|
577
644
|
|
|
578
645
|
# --- Issue tools ---
|
|
579
646
|
|
|
@@ -588,7 +655,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
588
655
|
state: str = "open",
|
|
589
656
|
labels: Optional[str] = None,
|
|
590
657
|
max_results: int = 30,
|
|
591
|
-
) ->
|
|
658
|
+
) -> ToolResult:
|
|
592
659
|
"""List issues in a repository.
|
|
593
660
|
|
|
594
661
|
Args:
|
|
@@ -608,10 +675,13 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
608
675
|
repo, state=state, labels=label_list, per_page=max_results,
|
|
609
676
|
)
|
|
610
677
|
except GitHubClientError as e:
|
|
611
|
-
return f"Could not list issues: {e}"
|
|
678
|
+
return ToolResult.failed(error=f"Could not list issues: {e}")
|
|
612
679
|
|
|
613
680
|
if not issues:
|
|
614
|
-
return
|
|
681
|
+
return ToolResult.ok(
|
|
682
|
+
f"No {state} issues found in {repo}",
|
|
683
|
+
data={"repo": repo, "state": state, "count": 0},
|
|
684
|
+
)
|
|
615
685
|
|
|
616
686
|
lines = [f"# Issues in {repo} ({state})\n"]
|
|
617
687
|
for issue in issues:
|
|
@@ -630,7 +700,10 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
630
700
|
lines.append(line)
|
|
631
701
|
|
|
632
702
|
lines.append(f"\n*{len(issues)} issue(s) shown*")
|
|
633
|
-
return
|
|
703
|
+
return ToolResult.ok(
|
|
704
|
+
"\n".join(lines),
|
|
705
|
+
data={"repo": repo, "state": state, "count": len(issues)},
|
|
706
|
+
)
|
|
634
707
|
|
|
635
708
|
@tool(
|
|
636
709
|
name="get_github_issue",
|
|
@@ -641,7 +714,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
641
714
|
self,
|
|
642
715
|
issue_number: int,
|
|
643
716
|
repo: str = "self",
|
|
644
|
-
) ->
|
|
717
|
+
) -> ToolResult:
|
|
645
718
|
"""Get a specific issue.
|
|
646
719
|
|
|
647
720
|
Args:
|
|
@@ -656,7 +729,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
656
729
|
try:
|
|
657
730
|
issue = await self.client.get_issue(repo, issue_number)
|
|
658
731
|
except GitHubClientError as e:
|
|
659
|
-
return f"Could not get issue #{issue_number}: {e}"
|
|
732
|
+
return ToolResult.failed(error=f"Could not get issue #{issue_number}: {e}")
|
|
660
733
|
|
|
661
734
|
title = issue.get("title", "")
|
|
662
735
|
state = issue.get("state", "")
|
|
@@ -685,7 +758,21 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
685
758
|
lines.append(f"**Comments:** {comments_count}")
|
|
686
759
|
lines.append(f"\n---\n\n{body}")
|
|
687
760
|
|
|
688
|
-
return
|
|
761
|
+
return ToolResult.ok(
|
|
762
|
+
"\n".join(lines),
|
|
763
|
+
data={
|
|
764
|
+
"repo": repo,
|
|
765
|
+
"number": issue_number,
|
|
766
|
+
"title": title,
|
|
767
|
+
"state": state,
|
|
768
|
+
"author": author,
|
|
769
|
+
"labels": issue_labels,
|
|
770
|
+
"assignees": assignees,
|
|
771
|
+
"milestone": milestone_name,
|
|
772
|
+
"comments_count": comments_count,
|
|
773
|
+
"url": issue.get("html_url"),
|
|
774
|
+
},
|
|
775
|
+
)
|
|
689
776
|
|
|
690
777
|
@tool(
|
|
691
778
|
name="get_github_issue_comments",
|
|
@@ -697,7 +784,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
697
784
|
issue_number: int,
|
|
698
785
|
repo: str = "self",
|
|
699
786
|
max_results: int = 30,
|
|
700
|
-
) ->
|
|
787
|
+
) -> ToolResult:
|
|
701
788
|
"""Get comments on an issue.
|
|
702
789
|
|
|
703
790
|
Args:
|
|
@@ -715,10 +802,13 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
715
802
|
repo, issue_number, per_page=max_results,
|
|
716
803
|
)
|
|
717
804
|
except GitHubClientError as e:
|
|
718
|
-
return f"Could not get comments for issue #{issue_number}: {e}"
|
|
805
|
+
return ToolResult.failed(error=f"Could not get comments for issue #{issue_number}: {e}")
|
|
719
806
|
|
|
720
807
|
if not comments:
|
|
721
|
-
return
|
|
808
|
+
return ToolResult.ok(
|
|
809
|
+
f"No comments on issue #{issue_number} in {repo}",
|
|
810
|
+
data={"repo": repo, "number": issue_number, "count": 0},
|
|
811
|
+
)
|
|
722
812
|
|
|
723
813
|
lines = [f"# Comments on #{issue_number} in {repo}\n"]
|
|
724
814
|
for comment in comments:
|
|
@@ -731,4 +821,7 @@ Use `list_source_components` to see the feature components that make up this age
|
|
|
731
821
|
lines.append("")
|
|
732
822
|
|
|
733
823
|
lines.append(f"\n*{len(comments)} comment(s)*")
|
|
734
|
-
return
|
|
824
|
+
return ToolResult.ok(
|
|
825
|
+
"\n".join(lines),
|
|
826
|
+
data={"repo": repo, "number": issue_number, "count": len(comments)},
|
|
827
|
+
)
|
|
@@ -320,7 +320,8 @@ class TestGitHubFeature:
|
|
|
320
320
|
|
|
321
321
|
result = await feature.read_github_file(repo="self", path="test.py")
|
|
322
322
|
|
|
323
|
-
assert "cached content" in result
|
|
323
|
+
assert "cached content" in result.confirmation
|
|
324
|
+
assert result.data["cached"] is True
|
|
324
325
|
|
|
325
326
|
@pytest.mark.asyncio
|
|
326
327
|
async def test_get_definition_non_python(self, feature):
|
|
@@ -330,4 +331,33 @@ class TestGitHubFeature:
|
|
|
330
331
|
name="something"
|
|
331
332
|
)
|
|
332
333
|
|
|
333
|
-
assert "only supports Python" in result
|
|
334
|
+
assert "only supports Python" in result.error
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ============== Contract Tests ==============
|
|
338
|
+
|
|
339
|
+
def test_all_tool_methods_return_toolresult():
|
|
340
|
+
"""Guard against regressing to str returns: the GitHubFeature module is on
|
|
341
|
+
the sovereign migrated-feature allowlist, so every @tool method MUST be
|
|
342
|
+
annotated ``-> ToolResult`` or registration hard-fails in the host."""
|
|
343
|
+
import typing
|
|
344
|
+
|
|
345
|
+
from kestrel_sdk.tools.result import ToolResult
|
|
346
|
+
|
|
347
|
+
tool_methods = [
|
|
348
|
+
"read_github_file",
|
|
349
|
+
"list_github_files",
|
|
350
|
+
"search_github_code",
|
|
351
|
+
"get_code_definition",
|
|
352
|
+
"list_code_definitions",
|
|
353
|
+
"get_self_repo_info",
|
|
354
|
+
"list_source_components",
|
|
355
|
+
"get_component_source",
|
|
356
|
+
"invalidate_github_cache",
|
|
357
|
+
"list_github_issues",
|
|
358
|
+
"get_github_issue",
|
|
359
|
+
"get_github_issue_comments",
|
|
360
|
+
]
|
|
361
|
+
for name in tool_methods:
|
|
362
|
+
hints = typing.get_type_hints(getattr(GitHubFeature, name))
|
|
363
|
+
assert hints.get("return") is ToolResult, f"{name} must return ToolResult"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/SKILL.md
RENAMED
|
File without changes
|
{kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/__init__.py
RENAMED
|
File without changes
|
{kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/ast_analyzer.py
RENAMED
|
File without changes
|
{kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/cache.py
RENAMED
|
File without changes
|
{kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/client.py
RENAMED
|
File without changes
|
{kestrel_feature_github-0.1.0 → kestrel_feature_github-0.1.1}/kestrel_feature_github/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|