Skip to content

Commit 2c9e108

Browse files
author
bgagent
committed
chore(agent): pydantic models
1 parent 546311e commit 2c9e108

16 files changed

Lines changed: 867 additions & 438 deletions

agent/src/config.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import uuid
66

7-
from models import TaskType
7+
from models import TaskConfig, TaskType
88

99
AGENT_WORKSPACE = os.environ.get("AGENT_WORKSPACE", "/workspace")
1010

@@ -52,54 +52,60 @@ def build_config(
5252
task_type: str = "new_task",
5353
branch_name: str = "",
5454
pr_number: str = "",
55-
) -> dict:
55+
) -> TaskConfig:
5656
"""Build and validate configuration from explicit parameters.
5757
5858
Parameters fall back to environment variables if empty.
5959
"""
60-
config = {
61-
"repo_url": repo_url or os.environ.get("REPO_URL", ""),
62-
"issue_number": issue_number or os.environ.get("ISSUE_NUMBER", ""),
63-
"task_description": task_description or os.environ.get("TASK_DESCRIPTION", ""),
64-
"github_token": github_token or resolve_github_token(),
65-
"aws_region": aws_region or os.environ.get("AWS_REGION", ""),
66-
"anthropic_model": anthropic_model
67-
or os.environ.get("ANTHROPIC_MODEL", "us.anthropic.claude-sonnet-4-6"),
68-
"dry_run": dry_run,
69-
"max_turns": max_turns,
70-
"max_budget_usd": max_budget_usd,
71-
"system_prompt_overrides": system_prompt_overrides,
72-
"task_type": task_type,
73-
"branch_name": branch_name,
74-
"pr_number": pr_number,
75-
}
60+
resolved_repo_url = repo_url or os.environ.get("REPO_URL", "")
61+
resolved_issue_number = issue_number or os.environ.get("ISSUE_NUMBER", "")
62+
resolved_task_description = task_description or os.environ.get("TASK_DESCRIPTION", "")
63+
resolved_github_token = github_token or resolve_github_token()
64+
resolved_aws_region = aws_region or os.environ.get("AWS_REGION", "")
65+
resolved_anthropic_model = anthropic_model or os.environ.get(
66+
"ANTHROPIC_MODEL", "us.anthropic.claude-sonnet-4-6"
67+
)
7668

7769
errors = []
78-
if not config["repo_url"]:
70+
if not resolved_repo_url:
7971
errors.append("repo_url is required (e.g., 'owner/repo')")
80-
if not config["github_token"]:
72+
if not resolved_github_token:
8173
errors.append("github_token is required")
82-
if not config["aws_region"]:
74+
if not resolved_aws_region:
8375
errors.append("aws_region is required for Bedrock")
8476
try:
85-
task = TaskType(config["task_type"])
77+
task = TaskType(task_type)
8678
except ValueError:
87-
errors.append(f"Invalid task_type: '{config['task_type']}'")
79+
errors.append(f"Invalid task_type: '{task_type}'")
8880
task = None
8981
if task and task.is_pr_task:
90-
if not config["pr_number"]:
82+
if not pr_number:
9183
errors.append("pr_number is required for pr_iteration/pr_review task type")
92-
elif task and not config["issue_number"] and not config["task_description"]:
84+
elif task and not resolved_issue_number and not resolved_task_description:
9385
errors.append("Either issue_number or task_description is required")
9486

9587
if errors:
9688
raise ValueError("; ".join(errors))
9789

98-
config["task_id"] = task_id or uuid.uuid4().hex[:12]
99-
return config
100-
101-
102-
def get_config() -> dict:
90+
return TaskConfig(
91+
repo_url=resolved_repo_url,
92+
issue_number=resolved_issue_number,
93+
task_description=resolved_task_description,
94+
github_token=resolved_github_token,
95+
aws_region=resolved_aws_region,
96+
anthropic_model=resolved_anthropic_model,
97+
dry_run=dry_run,
98+
max_turns=max_turns,
99+
max_budget_usd=max_budget_usd,
100+
system_prompt_overrides=system_prompt_overrides,
101+
task_type=task_type,
102+
branch_name=branch_name,
103+
pr_number=pr_number,
104+
task_id=task_id or uuid.uuid4().hex[:12],
105+
)
106+
107+
108+
def get_config() -> TaskConfig:
103109
"""Parse configuration from environment variables (local batch mode)."""
104110
try:
105111
return build_config(

agent/src/context.py

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import requests
44

5+
from models import GitHubIssue, IssueComment, TaskConfig
56

6-
def fetch_github_issue(repo_url: str, issue_number: str, token: str) -> dict:
7+
8+
def fetch_github_issue(repo_url: str, issue_number: str, token: str) -> GitHubIssue:
79
"""Fetch a GitHub issue's title, body, and comments."""
810
headers = {
911
"Authorization": f"token {token}",
@@ -20,51 +22,54 @@ def fetch_github_issue(repo_url: str, issue_number: str, token: str) -> dict:
2022
issue = issue_resp.json()
2123

2224
# Fetch comments
23-
comments = []
25+
comments: list[IssueComment] = []
2426
if issue.get("comments", 0) > 0:
2527
comments_resp = requests.get(
2628
f"https://api.github.com/repos/{repo_url}/issues/{issue_number}/comments",
2729
headers=headers,
2830
timeout=30,
2931
)
3032
comments_resp.raise_for_status()
31-
comments = [{"author": c["user"]["login"], "body": c["body"]} for c in comments_resp.json()]
33+
comments = [
34+
IssueComment(author=c["user"]["login"], body=c["body"]) for c in comments_resp.json()
35+
]
3236

33-
return {
34-
"title": issue["title"],
35-
"body": issue.get("body", ""),
36-
"number": issue["number"],
37-
"comments": comments,
38-
}
37+
return GitHubIssue(
38+
title=issue["title"],
39+
body=issue.get("body", "") or "",
40+
number=issue["number"],
41+
comments=comments,
42+
)
3943

4044

41-
def assemble_prompt(config: dict) -> str:
45+
def assemble_prompt(config: TaskConfig) -> str:
4246
"""Assemble the user prompt from issue context and task description.
4347
4448
.. deprecated::
4549
In production (AgentCore server mode), the orchestrator's
4650
``assembleUserPrompt()`` in ``context-hydration.ts`` is the sole prompt
47-
assembler. The hydrated prompt arrives via ``hydrated_context["user_prompt"]``.
51+
assembler. The hydrated prompt arrives via
52+
``HydratedContext.user_prompt`` (validated from the incoming JSON).
4853
This Python implementation is retained only for **local batch mode**
4954
(``python src/entrypoint.py``) and **dry-run mode** (``DRY_RUN=1``).
5055
"""
5156
parts = []
5257

53-
parts.append(f"Task ID: {config['task_id']}")
54-
parts.append(f"Repository: {config['repo_url']}")
58+
parts.append(f"Task ID: {config.task_id}")
59+
parts.append(f"Repository: {config.repo_url}")
5560

56-
if config.get("issue"):
57-
issue = config["issue"]
58-
parts.append(f"\n## GitHub Issue #{issue['number']}: {issue['title']}\n")
59-
parts.append(issue["body"] or "(no description)")
60-
if issue["comments"]:
61+
if config.issue:
62+
issue = config.issue
63+
parts.append(f"\n## GitHub Issue #{issue.number}: {issue.title}\n")
64+
parts.append(issue.body or "(no description)")
65+
if issue.comments:
6166
parts.append("\n### Comments\n")
62-
for c in issue["comments"]:
63-
parts.append(f"**@{c['author']}**: {c['body']}\n")
67+
for c in issue.comments:
68+
parts.append(f"**@{c.author}**: {c.body}\n")
6469

65-
if config["task_description"]:
66-
parts.append(f"\n## Task\n\n{config['task_description']}")
67-
elif config.get("issue"):
70+
if config.task_description:
71+
parts.append(f"\n## Task\n\n{config.task_description}")
72+
elif config.issue:
6873
parts.append(
6974
"\n## Task\n\nResolve the GitHub issue described above. "
7075
"Follow the workflow in your system instructions."

agent/src/entrypoint.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@
2020
resolve_github_token,
2121
)
2222
from context import assemble_prompt, fetch_github_issue # noqa: F401
23+
from models import ( # noqa: F401
24+
AgentResult,
25+
GitHubIssue,
26+
HydratedContext,
27+
IssueComment,
28+
MemoryContext,
29+
RepoSetup,
30+
TaskConfig,
31+
TaskResult,
32+
TaskType,
33+
TokenUsage,
34+
)
2335
from pipeline import main, run_task # noqa: F401
2436
from post_hooks import ( # noqa: F401
2537
ensure_committed,

agent/src/models.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Data models and enumerations for the agent pipeline."""
22

3+
from __future__ import annotations
4+
35
from enum import StrEnum
46

7+
from pydantic import BaseModel, ConfigDict
8+
59

610
class TaskType(StrEnum):
711
"""Supported task types."""
@@ -17,3 +21,114 @@ def is_pr_task(self) -> bool:
1721
@property
1822
def is_read_only(self) -> bool:
1923
return self == TaskType.pr_review
24+
25+
26+
class IssueComment(BaseModel):
27+
model_config = ConfigDict(frozen=True)
28+
29+
author: str
30+
body: str
31+
32+
33+
class GitHubIssue(BaseModel):
34+
model_config = ConfigDict(frozen=True)
35+
36+
title: str
37+
body: str = ""
38+
number: int
39+
comments: list[IssueComment] = []
40+
41+
42+
class MemoryContext(BaseModel):
43+
model_config = ConfigDict(frozen=True)
44+
45+
repo_knowledge: list[str] = []
46+
past_episodes: list[str] = []
47+
48+
49+
class HydratedContext(BaseModel):
50+
model_config = ConfigDict(frozen=True)
51+
52+
user_prompt: str
53+
issue: GitHubIssue | None = None
54+
resolved_base_branch: str | None = None
55+
truncated: bool = False
56+
memory_context: MemoryContext | None = None
57+
58+
59+
class TaskConfig(BaseModel):
60+
model_config = ConfigDict(validate_assignment=True)
61+
62+
repo_url: str
63+
issue_number: str = ""
64+
task_description: str = ""
65+
github_token: str
66+
aws_region: str
67+
anthropic_model: str = "us.anthropic.claude-sonnet-4-6"
68+
dry_run: bool = False
69+
max_turns: int = 10
70+
max_budget_usd: float | None = None
71+
system_prompt_overrides: str = ""
72+
task_type: str = "new_task"
73+
branch_name: str = ""
74+
pr_number: str = ""
75+
task_id: str = ""
76+
# Enriched mid-flight by pipeline.py:
77+
cedar_policies: list[str] = []
78+
issue: GitHubIssue | None = None
79+
base_branch: str | None = None
80+
81+
82+
class RepoSetup(BaseModel):
83+
model_config = ConfigDict(frozen=True)
84+
85+
repo_dir: str
86+
branch: str
87+
notes: list[str] = []
88+
build_before: bool = True
89+
lint_before: bool = True
90+
default_branch: str = "main"
91+
92+
93+
class TokenUsage(BaseModel):
94+
model_config = ConfigDict(frozen=True)
95+
96+
input_tokens: int = 0
97+
output_tokens: int = 0
98+
cache_read_input_tokens: int = 0
99+
cache_creation_input_tokens: int = 0
100+
101+
102+
class AgentResult(BaseModel):
103+
status: str = "unknown"
104+
turns: int = 0
105+
num_turns: int = 0
106+
cost_usd: float | None = None
107+
duration_ms: int = 0
108+
duration_api_ms: int = 0
109+
session_id: str = ""
110+
error: str | None = None
111+
usage: TokenUsage | None = None
112+
113+
114+
class TaskResult(BaseModel):
115+
status: str
116+
agent_status: str = "unknown"
117+
pr_url: str | None = None
118+
build_passed: bool = False
119+
lint_passed: bool = False
120+
cost_usd: float | None = None
121+
turns: int | None = None
122+
duration_s: float = 0.0
123+
task_id: str = ""
124+
disk_before: str = ""
125+
disk_after: str = ""
126+
disk_delta: str = ""
127+
prompt_version: str | None = None
128+
memory_written: bool = False
129+
error: str | None = None
130+
session_id: str | None = None
131+
input_tokens: int | None = None
132+
output_tokens: int | None = None
133+
cache_read_input_tokens: int | None = None
134+
cache_creation_input_tokens: int | None = None

0 commit comments

Comments
 (0)