Skip to content

Commit 8f03cc1

Browse files
authored
Merge branch 'main' into fix/streaming-input-guardrail-tripwire
2 parents 60856f6 + f61d0c1 commit 8f03cc1

5 files changed

Lines changed: 129 additions & 16 deletions

File tree

.github/codex/prompts/pr-labels.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
You are Codex running in CI to propose labels for a pull request in the openai-agents-python repository.
44

55
Inputs:
6+
- PR context: .tmp/pr-labels/pr-context.json
67
- PR diff: .tmp/pr-labels/changes.diff
78
- Changed files: .tmp/pr-labels/changed-files.txt
89

910
Task:
10-
- Inspect the diff and changed files.
11+
- Inspect the PR context, diff, and changed files.
1112
- Output JSON with a single top-level key: "labels" (array of strings).
1213
- Only use labels from the allowed list.
1314
- Prefer false negatives over false positives. If you are unsure, leave the label out.
@@ -53,7 +54,7 @@ Label rules:
5354
- feature:voice: Voice pipeline behavior is a primary deliverable of the PR.
5455

5556
Decision process:
56-
1. Determine the PR's primary intent in one sentence from the title and dominant runtime diff.
57+
1. Determine the PR's primary intent in one sentence from the PR title/body and dominant runtime diff.
5758
2. Start with zero labels.
5859
3. Add `bug` or `enhancement` conservatively.
5960
4. Add only the minimum `feature:*` labels needed to describe the primary surface area.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"type": "object",
3+
"additionalProperties": false,
4+
"required": ["labels"],
5+
"properties": {
6+
"labels": {
7+
"type": "array",
8+
"uniqueItems": true,
9+
"items": {
10+
"type": "string",
11+
"enum": [
12+
"documentation",
13+
"project",
14+
"bug",
15+
"enhancement",
16+
"dependencies",
17+
"feature:chat-completions",
18+
"feature:core",
19+
"feature:lite-llm",
20+
"feature:mcp",
21+
"feature:realtime",
22+
"feature:sessions",
23+
"feature:tracing",
24+
"feature:voice"
25+
]
26+
}
27+
}
28+
}
29+
}

.github/scripts/pr_labels.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@
2626
"feature:voice",
2727
}
2828

29+
DETERMINISTIC_LABELS: Final[set[str]] = {
30+
"documentation",
31+
"project",
32+
"dependencies",
33+
}
34+
35+
MODEL_ONLY_LABELS: Final[set[str]] = {
36+
"bug",
37+
"enhancement",
38+
}
39+
40+
FEATURE_LABELS: Final[set[str]] = ALLOWED_LABELS - DETERMINISTIC_LABELS - MODEL_ONLY_LABELS
41+
2942
SOURCE_FEATURE_PREFIXES: Final[dict[str, tuple[str, ...]]] = {
3043
"feature:realtime": ("src/agents/realtime/",),
3144
"feature:voice": ("src/agents/voice/",),
@@ -201,27 +214,30 @@ def load_json(path: pathlib.Path) -> Any:
201214
return json.loads(path.read_text())
202215

203216

204-
def load_codex_labels(path: pathlib.Path) -> list[str]:
217+
def load_codex_labels(path: pathlib.Path) -> tuple[list[str], bool]:
205218
if not path.exists():
206-
return []
219+
return [], False
207220

208221
raw = path.read_text().strip()
209222
if not raw:
210-
return []
223+
return [], False
211224

212225
try:
213226
payload = load_json(path)
214227
except json.JSONDecodeError:
215-
return []
228+
return [], False
216229

217230
if not isinstance(payload, dict):
218-
return []
231+
return [], False
219232

220-
labels = payload.get("labels", [])
233+
labels = payload.get("labels")
221234
if not isinstance(labels, list):
222-
return []
235+
return [], False
223236

224-
return [label for label in labels if isinstance(label, str)]
237+
if not all(isinstance(label, str) for label in labels):
238+
return [], False
239+
240+
return list(labels), True
225241

226242

227243
def fetch_existing_labels(pr_number: str) -> set[str]:
@@ -237,6 +253,7 @@ def compute_desired_labels(
237253
changed_files: Sequence[str],
238254
diff_text: str,
239255
codex_ran: bool,
256+
codex_output_valid: bool,
240257
codex_labels: Sequence[str],
241258
base_sha: str | None,
242259
head_sha: str | None,
@@ -257,7 +274,7 @@ def compute_desired_labels(
257274
if dependencies_allowed:
258275
desired.add("dependencies")
259276

260-
if codex_ran:
277+
if codex_ran and codex_output_valid:
261278
for label in codex_labels:
262279
if label == "dependencies" and not dependencies_allowed:
263280
continue
@@ -269,6 +286,13 @@ def compute_desired_labels(
269286
return desired
270287

271288

289+
def compute_managed_labels(*, codex_ran: bool, codex_output_valid: bool) -> set[str]:
290+
managed = DETERMINISTIC_LABELS | FEATURE_LABELS
291+
if codex_ran and codex_output_valid:
292+
managed |= MODEL_ONLY_LABELS
293+
return managed
294+
295+
272296
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
273297
parser = argparse.ArgumentParser()
274298
parser.add_argument("--pr-number", default=os.environ.get("PR_NUMBER", ""))
@@ -308,18 +332,29 @@ def main(argv: Sequence[str] | None = None) -> int:
308332
]
309333

310334
diff_text = changes_diff_path.read_text() if changes_diff_path.exists() else ""
335+
codex_labels, codex_output_valid = load_codex_labels(codex_output_path)
336+
if codex_ran and not codex_output_valid:
337+
print(
338+
"Codex output missing or invalid; using fallback feature labels and preserving "
339+
"model-only labels."
340+
)
311341
desired = compute_desired_labels(
312342
changed_files=changed_files,
313343
diff_text=diff_text,
314344
codex_ran=codex_ran,
315-
codex_labels=load_codex_labels(codex_output_path),
345+
codex_output_valid=codex_output_valid,
346+
codex_labels=codex_labels,
316347
base_sha=args.base_sha or None,
317348
head_sha=args.head_sha or None,
318349
)
319350

320351
existing = fetch_existing_labels(args.pr_number)
352+
managed_labels = compute_managed_labels(
353+
codex_ran=codex_ran,
354+
codex_output_valid=codex_output_valid,
355+
)
321356
to_add = sorted(desired - existing)
322-
to_remove = sorted((existing & ALLOWED_LABELS) - desired)
357+
to_remove = sorted((existing & managed_labels) - desired)
323358

324359
if not to_add and not to_remove:
325360
print("Labels already up to date.")

.github/workflows/pr-labels.yml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ jobs:
6565
core.setOutput('head_sha', pr.head.sha);
6666
core.setOutput('head_repo', headRepo);
6767
core.setOutput('is_fork', headRepo !== repoFullName);
68+
core.setOutput('title', pr.title || '');
69+
core.setOutput('body', pr.body || '');
6870
6971
- name: Checkout base
7072
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
@@ -81,14 +83,36 @@ jobs:
8183
"https://github.com/${PR_HEAD_REPO}.git" \
8284
"${PR_HEAD_SHA}"
8385
- name: Collect PR diff
86+
id: diff
8487
env:
8588
PR_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
8689
PR_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
90+
PR_TITLE: ${{ steps.pr.outputs.title }}
91+
PR_BODY: ${{ steps.pr.outputs.body }}
8792
run: |
8893
set -euo pipefail
8994
mkdir -p .tmp/pr-labels
90-
git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" > .tmp/pr-labels/changed-files.txt
91-
git diff "$PR_BASE_SHA" "$PR_HEAD_SHA" > .tmp/pr-labels/changes.diff
95+
diff_base_sha="$(git merge-base "$PR_BASE_SHA" "$PR_HEAD_SHA")"
96+
echo "diff_base_sha=${diff_base_sha}" >> "$GITHUB_OUTPUT"
97+
git diff --name-only "$diff_base_sha" "$PR_HEAD_SHA" > .tmp/pr-labels/changed-files.txt
98+
git diff "$diff_base_sha" "$PR_HEAD_SHA" > .tmp/pr-labels/changes.diff
99+
python - <<'PY'
100+
import json
101+
import os
102+
import pathlib
103+
104+
pathlib.Path(".tmp/pr-labels/pr-context.json").write_text(
105+
json.dumps(
106+
{
107+
"title": os.environ.get("PR_TITLE", ""),
108+
"body": os.environ.get("PR_BODY", ""),
109+
},
110+
ensure_ascii=False,
111+
indent=2,
112+
)
113+
+ "\n"
114+
)
115+
PY
92116
- name: Prepare Codex output
93117
id: codex-output
94118
run: |
@@ -105,13 +129,14 @@ jobs:
105129
openai-api-key: ${{ secrets.PROD_OPENAI_API_KEY }}
106130
prompt-file: .github/codex/prompts/pr-labels.md
107131
output-file: ${{ steps.codex-output.outputs.output_file }}
132+
output-schema-file: .github/codex/schemas/pr-labels.json
108133
safety-strategy: drop-sudo
109134
sandbox: read-only
110135
- name: Apply labels
111136
env:
112137
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113138
PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
114-
PR_BASE_SHA: ${{ steps.pr.outputs.base_sha }}
139+
PR_BASE_SHA: ${{ steps.diff.outputs.diff_base_sha }}
115140
PR_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
116141
CODEX_OUTPUT_PATH: ${{ steps.codex-output.outputs.output_file }}
117142
CODEX_CONCLUSION: ${{ steps.run_codex.conclusion }}

tests/test_pr_labels.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,32 @@ def test_compute_desired_labels_removes_stale_fallback_labels() -> None:
4343
changed_files=["src/agents/models/chatcmpl_converter.py"],
4444
diff_text="",
4545
codex_ran=False,
46+
codex_output_valid=False,
4647
codex_labels=[],
4748
base_sha=None,
4849
head_sha=None,
4950
)
5051

5152
assert desired == {"feature:chat-completions"}
53+
54+
55+
def test_compute_desired_labels_falls_back_when_codex_output_is_invalid() -> None:
56+
desired = pr_labels.compute_desired_labels(
57+
changed_files=["src/agents/run_internal/approvals.py"],
58+
diff_text="",
59+
codex_ran=True,
60+
codex_output_valid=False,
61+
codex_labels=[],
62+
base_sha=None,
63+
head_sha=None,
64+
)
65+
66+
assert desired == {"feature:core"}
67+
68+
69+
def test_compute_managed_labels_preserves_model_only_labels_without_valid_codex_output() -> None:
70+
managed = pr_labels.compute_managed_labels(codex_ran=True, codex_output_valid=False)
71+
72+
assert "bug" not in managed
73+
assert "enhancement" not in managed
74+
assert "feature:core" in managed

0 commit comments

Comments
 (0)