@openhands/extensions 0.29.0 → 0.30.0
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.
- package/.agents/skills/custom-codereview-guide.md +44 -1
- package/.claude-plugin/marketplace.json +7 -5
- package/.codex-plugin/marketplace.json +7 -5
- package/.github/workflows/build-vscode-app.yml +3 -3
- package/.github/workflows/check-extensions.yml +1 -1
- package/.github/workflows/tests.yml +1 -1
- package/.plugin/marketplace.json +7 -5
- package/.release-please-manifest.json +1 -1
- package/README.md +3 -3
- package/apps/vscode/README.md +8 -6
- package/apps/vscode/canvas-extension.json +11 -3
- package/apps/vscode/extension.js +60 -12
- package/apps/vscode/scripts/package.mjs +20 -74
- package/automations/bundle-index.js +9 -7
- package/automations/catalog/github-issue-to-pr/manifest.json +1 -1
- package/automations/catalog/github-issue-triage/manifest.json +4 -3
- package/automations/catalog/github-pr-reviewer/manifest.json +5 -4
- package/automations/catalog/gitlab-issue-to-mr/manifest.json +3 -3
- package/automations/catalog/jira-issue-to-bitbucket-pr/manifest.json +4 -4
- package/automations/catalog/jira-issue-to-gitlab-mr/manifest.json +5 -5
- package/automations/catalog/jira-issue-to-pr/manifest.json +1 -1
- package/integrations/catalog/atlassian-rovo.json +4 -3
- package/integrations/catalog/bitbucket.json +16 -1
- package/integrations/catalog/github.json +1 -0
- package/integrations/catalog/gitlab.json +1 -1
- package/marketplaces/openhands-extensions.json +7 -5
- package/package.json +1 -1
- package/plugins/pr-review/README.md +1 -0
- package/plugins/pr-review/scripts/prompt.py +44 -11
- package/plugins/runtime-api-configs/SKILL.md +1 -1
- package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +224 -8
- package/pyproject.toml +1 -1
- package/python/openhands_extensions/_version.py +1 -1
- package/skills/canvas-extension-api/references/backend-safety.md +3 -3
- package/skills/canvas-extension-api/references/v1-contract.md +7 -4
- package/skills/canvas-extension-api/scripts/validate-extension.mjs +17 -1
- package/skills/canvas-extension-api/scripts/validate-extension.test.mjs +49 -0
- package/skills/github/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-to-pr/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/README.md +73 -14
- package/skills/github-issue-triage/SKILL.md +26 -46
- package/skills/github-issue-triage/scripts/agent_conversation.py +224 -8
- package/skills/github-issue-triage/scripts/triage_publication.py +293 -0
- package/skills/github-issue-triage/scripts/worker.py +153 -103
- package/skills/github-pr-reviewer/README.md +103 -1
- package/skills/github-pr-reviewer/SKILL.md +35 -8
- package/skills/github-pr-reviewer/references/state-schema.md +8 -0
- package/skills/github-pr-reviewer/scripts/agent_conversation.py +224 -8
- package/skills/github-pr-reviewer/scripts/main.py +163 -39
- package/skills/github-pr-reviewer/scripts/publication.py +193 -0
- package/skills/github-pr-reviewer/scripts/worker.py +297 -8
- package/skills/github-pr-reviewer/tests/test_main.py +73 -5
- package/skills/gitlab-issue-to-mr/README.md +10 -3
- package/skills/gitlab-issue-to-mr/SKILL.md +43 -9
- package/skills/gitlab-issue-to-mr/references/state-schema.md +4 -0
- package/skills/gitlab-issue-to-mr/scripts/main.py +359 -25
- package/skills/index.js +8 -8
- package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/.plugin/plugin.json +4 -1
- package/skills/jira-issue-to-pr/README.md +18 -11
- package/skills/jira-issue-to-pr/SKILL.md +72 -23
- package/skills/jira-issue-to-pr/references/setup.md +43 -12
- package/skills/jira-issue-to-pr/scripts/main.py +136 -26
- package/skills/slack-channel-monitor/README.md +6 -1
- package/skills/slack-channel-monitor/SKILL.md +6 -3
- package/skills/slack-channel-monitor/scripts/main.py +36 -4
- package/skills/ticket-to-code-change/.claude-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.codex-plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/.plugin/plugin.json +1 -2
- package/skills/ticket-to-code-change/SKILL.md +5 -3
- package/skills/ticket-to-code-change/commands/ticket-to-code-change-setup.md +1 -1
- package/tests/fixtures/automations/github-pr-reviewer.json +9 -9
- package/tests/test_agent_conversation_dispatch.py +173 -6
- package/tests/test_automation_llm_provenance.py +1209 -0
- package/tests/test_github_review_publication.py +422 -0
- package/tests/test_github_reviewer_delivery.py +174 -21
- package/tests/test_github_triage_delivery.py +411 -238
- package/tests/test_gitlab_issue_to_mr.py +284 -0
- package/tests/test_jira_issue_to_pr.py +200 -6
- package/tests/test_pr_review_prompt_injection.py +353 -0
- package/tests/test_slack_channel_monitor.py +32 -1
- package/tests/test_vscode_app.py +8 -2
- package/tests/vscode-app.test.mjs +8 -2
|
@@ -33,6 +33,7 @@ Optional secret:
|
|
|
33
33
|
|
|
34
34
|
import json
|
|
35
35
|
import os
|
|
36
|
+
import re
|
|
36
37
|
import sys
|
|
37
38
|
import time
|
|
38
39
|
import urllib.error
|
|
@@ -577,7 +578,12 @@ def _build_secrets_payload(agent_url: str, api_key: str) -> dict:
|
|
|
577
578
|
return secrets
|
|
578
579
|
|
|
579
580
|
|
|
580
|
-
def create_conversation(
|
|
581
|
+
def create_conversation(
|
|
582
|
+
agent_url: str,
|
|
583
|
+
api_key: str,
|
|
584
|
+
initial_message: str,
|
|
585
|
+
agent: dict | None = None,
|
|
586
|
+
) -> str:
|
|
581
587
|
"""Create a conversation and return its ID.
|
|
582
588
|
|
|
583
589
|
The server auto-starts the agent when initial_message is provided
|
|
@@ -597,10 +603,9 @@ def create_conversation(agent_url: str, api_key: str, initial_message: str) -> s
|
|
|
597
603
|
workspace_dir = os.path.join(root, "slack-monitor-conversations")
|
|
598
604
|
os.makedirs(workspace_dir, exist_ok=True)
|
|
599
605
|
|
|
600
|
-
agent = _get_agent_dict(agent_url, api_key)
|
|
601
606
|
payload: dict = {
|
|
602
607
|
"workspace": {"working_dir": workspace_dir},
|
|
603
|
-
"agent": agent,
|
|
608
|
+
"agent": agent or _get_agent_dict(agent_url, api_key),
|
|
604
609
|
"initial_message": {"content": [{"text": initial_message}]},
|
|
605
610
|
}
|
|
606
611
|
|
|
@@ -639,6 +644,20 @@ def conversation_final_response(agent_url: str, api_key: str, conv_id: str) -> s
|
|
|
639
644
|
return result.get("response", "")
|
|
640
645
|
|
|
641
646
|
|
|
647
|
+
def _llm_provenance(profile: str, model: str) -> str:
|
|
648
|
+
return f"LLM profile: `{profile}` · Model: `{model}`"
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _with_llm_provenance(body: str, profile: str, model: str) -> str:
|
|
652
|
+
provenance = _llm_provenance(profile, model)
|
|
653
|
+
body = "\n".join(
|
|
654
|
+
line
|
|
655
|
+
for line in (body or "").splitlines()
|
|
656
|
+
if not re.fullmatch(r"LLM profile: .* · Model: .*", line.strip())
|
|
657
|
+
).strip()
|
|
658
|
+
return f"{body}\n\n{provenance}" if body else provenance
|
|
659
|
+
|
|
660
|
+
|
|
642
661
|
# ── Message filtering ──────────────────────────────────────────────────────────
|
|
643
662
|
|
|
644
663
|
def _is_human_message(msg: dict, bot_user_id: str, bot_message_ts: list[str]) -> bool:
|
|
@@ -937,7 +956,10 @@ def _process_trigger_message(
|
|
|
937
956
|
)
|
|
938
957
|
|
|
939
958
|
try:
|
|
940
|
-
|
|
959
|
+
agent = _get_agent_dict(agent_url, api_key)
|
|
960
|
+
conv_id = create_conversation(
|
|
961
|
+
agent_url, api_key, initial_prompt, agent=agent
|
|
962
|
+
)
|
|
941
963
|
conv_url = f"{openhands_url}/conversations/{conv_id}"
|
|
942
964
|
|
|
943
965
|
now = time.time()
|
|
@@ -946,6 +968,8 @@ def _process_trigger_message(
|
|
|
946
968
|
"channel_id": channel_id,
|
|
947
969
|
"thread_ts": thread_root,
|
|
948
970
|
"status": "active",
|
|
971
|
+
"llm_profile": "default",
|
|
972
|
+
"llm_model": agent["llm"].get("model") or "unknown",
|
|
949
973
|
"last_activity": now,
|
|
950
974
|
"last_seen_reply_ts": msg_ts,
|
|
951
975
|
"reply_poll_backoff_seconds": THREAD_REPLY_INITIAL_BACKOFF_SECONDS,
|
|
@@ -1003,6 +1027,14 @@ def _check_conversation_completion(
|
|
|
1003
1027
|
)
|
|
1004
1028
|
else:
|
|
1005
1029
|
summary = final if final else "Success (no message available)."
|
|
1030
|
+
# The values recorded when this script started the conversation; a
|
|
1031
|
+
# thread follow-up reuses it, so they still describe its LLM. A record
|
|
1032
|
+
# written before provenance was tracked cannot name either value.
|
|
1033
|
+
summary = _with_llm_provenance(
|
|
1034
|
+
summary,
|
|
1035
|
+
rec.get("llm_profile") or "unknown",
|
|
1036
|
+
rec.get("llm_model") or "unknown",
|
|
1037
|
+
)
|
|
1006
1038
|
|
|
1007
1039
|
ts_back = post_message(slack_token, channel_id, summary, thread_ts=thread_ts)
|
|
1008
1040
|
if ts_back:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticket-to-code-change",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Create pull or merge requests from
|
|
4
|
+
"description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "OpenHands",
|
|
7
7
|
"email": "contact@all-hands.dev"
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"repository": "https://github.com/OpenHands/extensions",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
|
-
"jira",
|
|
14
13
|
"linear",
|
|
15
14
|
"github",
|
|
16
15
|
"gitlab",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticket-to-code-change",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Create pull or merge requests from
|
|
4
|
+
"description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "OpenHands",
|
|
7
7
|
"email": "contact@all-hands.dev"
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"repository": "https://github.com/OpenHands/extensions",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
|
-
"jira",
|
|
14
13
|
"linear",
|
|
15
14
|
"github",
|
|
16
15
|
"gitlab",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ticket-to-code-change",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "Create pull or merge requests from
|
|
4
|
+
"description": "Create pull or merge requests from Linear tickets across GitHub, GitLab, and Bitbucket.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "OpenHands",
|
|
7
7
|
"email": "contact@all-hands.dev"
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"repository": "https://github.com/OpenHands/extensions",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"keywords": [
|
|
13
|
-
"jira",
|
|
14
13
|
"linear",
|
|
15
14
|
"github",
|
|
16
15
|
"gitlab",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ticket-to-code-change
|
|
3
3
|
description: >
|
|
4
|
-
Set up a ticket-to-code-change automation using
|
|
5
|
-
|
|
4
|
+
Set up a ticket-to-code-change automation using Linear as the issue tracker
|
|
5
|
+
and GitHub, GitLab, or Bitbucket as the source-control provider. The Jira
|
|
6
|
+
templates are served by the jira-issue-to-pr skill instead.
|
|
6
7
|
Watches for implementation-ready tickets, starts an OpenHands conversation
|
|
7
8
|
to implement and test the request, opens a pull or merge request, and links
|
|
8
9
|
the result back to the ticket.
|
|
@@ -19,7 +20,8 @@ requests or merge requests.
|
|
|
19
20
|
|
|
20
21
|
Ask the user for:
|
|
21
22
|
|
|
22
|
-
1. The issue tracker: Jira
|
|
23
|
+
1. The issue tracker: Linear. Jira automations are set up with the
|
|
24
|
+
`jira-issue-to-pr` skill instead, which ships a poller.
|
|
23
25
|
2. The source-control provider: GitHub, GitLab, or Bitbucket Cloud.
|
|
24
26
|
3. The project or team to watch and the label or workflow state that means
|
|
25
27
|
"implementation ready".
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
# auto-generated by sync_extensions.py
|
|
3
|
-
description: Set up a ticket-to-code-change automation using
|
|
3
|
+
description: Set up a ticket-to-code-change automation using Linear as the issue tracker and GitHub, GitLab, or Bitbucket as the source-control provider. The Jira templates are served by the jira-issue-to-pr skill instead. Watches for implementation-ready tickets, starts an OpenHands conversation to implement and test the request, opens a pull or merge request, and links the result back to the ticket.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Read and follow the complete instructions in the SKILL.md file located in this skill's directory.
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"timeout": 300,
|
|
69
69
|
"template": {
|
|
70
70
|
"id": "github-pr-reviewer",
|
|
71
|
-
"version": "1.
|
|
71
|
+
"version": "1.11.1",
|
|
72
72
|
"config": {
|
|
73
73
|
"repos": [
|
|
74
74
|
"OpenHands/agent-server-gui"
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"timeout": 300,
|
|
109
109
|
"template": {
|
|
110
110
|
"id": "github-pr-reviewer",
|
|
111
|
-
"version": "1.
|
|
111
|
+
"version": "1.11.1",
|
|
112
112
|
"config": {
|
|
113
113
|
"repos": [
|
|
114
114
|
"OpenHands/agent-server-gui"
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"timeout": 300,
|
|
214
214
|
"template": {
|
|
215
215
|
"id": "github-pr-reviewer",
|
|
216
|
-
"version": "1.
|
|
216
|
+
"version": "1.11.1",
|
|
217
217
|
"config": {
|
|
218
218
|
"repos": [
|
|
219
219
|
"OpenHands/agent-server-gui",
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
"timeout": 300,
|
|
255
255
|
"template": {
|
|
256
256
|
"id": "github-pr-reviewer",
|
|
257
|
-
"version": "1.
|
|
257
|
+
"version": "1.11.1",
|
|
258
258
|
"config": {
|
|
259
259
|
"repos": [
|
|
260
260
|
"OpenHands/agent-server-gui",
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
"preset_metadata": {
|
|
297
297
|
"template": {
|
|
298
298
|
"id": "github-pr-reviewer",
|
|
299
|
-
"version": "1.
|
|
299
|
+
"version": "1.11.1",
|
|
300
300
|
"config": {
|
|
301
301
|
"repos": [
|
|
302
302
|
"OpenHands/agent-server-gui",
|
|
@@ -351,7 +351,7 @@
|
|
|
351
351
|
"timeout": 300,
|
|
352
352
|
"template": {
|
|
353
353
|
"id": "github-pr-reviewer",
|
|
354
|
-
"version": "1.
|
|
354
|
+
"version": "1.11.1",
|
|
355
355
|
"config": {
|
|
356
356
|
"repos": [
|
|
357
357
|
"OpenHands/agent-server-gui"
|
|
@@ -444,7 +444,7 @@
|
|
|
444
444
|
"timeout": 300,
|
|
445
445
|
"template": {
|
|
446
446
|
"id": "github-pr-reviewer",
|
|
447
|
-
"version": "1.
|
|
447
|
+
"version": "1.11.1",
|
|
448
448
|
"config": {
|
|
449
449
|
"repos": [
|
|
450
450
|
"OpenHands/agent-server-gui"
|
|
@@ -540,7 +540,7 @@
|
|
|
540
540
|
"timeout": 300,
|
|
541
541
|
"template": {
|
|
542
542
|
"id": "github-pr-reviewer",
|
|
543
|
-
"version": "1.
|
|
543
|
+
"version": "1.11.1",
|
|
544
544
|
"config": {
|
|
545
545
|
"repos": [
|
|
546
546
|
"OpenHands/agent-server-gui"
|
|
@@ -616,7 +616,7 @@
|
|
|
616
616
|
"timeout": 300,
|
|
617
617
|
"template": {
|
|
618
618
|
"id": "github-pr-reviewer",
|
|
619
|
-
"version": "1.
|
|
619
|
+
"version": "1.11.1",
|
|
620
620
|
"config": {
|
|
621
621
|
"repos": [
|
|
622
622
|
"OpenHands/automation"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from unittest.mock import MagicMock
|
|
3
|
+
from urllib.error import HTTPError
|
|
3
4
|
from uuid import NAMESPACE_URL, UUID, uuid5
|
|
4
5
|
|
|
5
6
|
import agent_conversation
|
|
@@ -53,19 +54,20 @@ def test_github_secret_falls_back_to_agent_server(monkeypatch):
|
|
|
53
54
|
workspace.reset_client.assert_called_once_with()
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
|
|
57
|
+
def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch, tmp_path):
|
|
57
58
|
state = {}
|
|
58
59
|
_fake_kv(monkeypatch, state)
|
|
59
60
|
monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
|
|
61
|
+
monkeypatch.setenv("WORKSPACE_BASE", str(tmp_path))
|
|
60
62
|
workspace = MagicMock()
|
|
61
63
|
workspace.__enter__.return_value = workspace
|
|
64
|
+
workspace.working_dir = str(tmp_path)
|
|
62
65
|
scoped_secrets = {
|
|
63
66
|
"GITHUB_TOKEN": LookupSecret(url="/api/settings/secrets/GITHUB_TOKEN")
|
|
64
67
|
}
|
|
65
68
|
workspace.get_secrets.return_value = scoped_secrets
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
+
remote_workspace = MagicMock(return_value=workspace)
|
|
70
|
+
monkeypatch.setattr(agent_conversation, "RemoteWorkspace", remote_workspace)
|
|
69
71
|
missing = agent_conversation.httpx.HTTPStatusError(
|
|
70
72
|
"missing",
|
|
71
73
|
request=MagicMock(),
|
|
@@ -82,6 +84,10 @@ def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
|
|
|
82
84
|
result = dispatcher.deliver("repo:issue:7", "revision-1", "work")
|
|
83
85
|
|
|
84
86
|
request = create.call_args.args[1]
|
|
87
|
+
remote_workspace.assert_called_once_with(
|
|
88
|
+
host="http://agent", api_key="session", working_dir=str(tmp_path)
|
|
89
|
+
)
|
|
90
|
+
assert request.workspace.working_dir == str(tmp_path)
|
|
85
91
|
assert request.agent_profile_id == UUID("11111111-1111-4111-8111-111111111111")
|
|
86
92
|
assert request.secrets == scoped_secrets
|
|
87
93
|
workspace.get_secrets.assert_called_once_with(
|
|
@@ -100,6 +106,32 @@ def test_new_subject_uses_selected_profile_and_persists_mapping(monkeypatch):
|
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
|
|
109
|
+
def test_review_work_uses_configured_local_workspace(monkeypatch, tmp_path):
|
|
110
|
+
monkeypatch.setattr(agent_conversation, "_register_tools", lambda: None)
|
|
111
|
+
monkeypatch.setenv("WORKSPACE_BASE", str(tmp_path))
|
|
112
|
+
workspace = MagicMock()
|
|
113
|
+
workspace.__enter__.return_value = workspace
|
|
114
|
+
workspace.working_dir = str(tmp_path)
|
|
115
|
+
workspace.get_secrets.return_value = {}
|
|
116
|
+
remote_workspace = MagicMock(return_value=workspace)
|
|
117
|
+
monkeypatch.setattr(agent_conversation, "RemoteWorkspace", remote_workspace)
|
|
118
|
+
conversation = MagicMock()
|
|
119
|
+
create = MagicMock(return_value=conversation)
|
|
120
|
+
monkeypatch.setattr(agent_conversation.RemoteConversation, "create", create)
|
|
121
|
+
|
|
122
|
+
with _dispatcher(monkeypatch) as dispatcher:
|
|
123
|
+
dispatcher.start_review_work(
|
|
124
|
+
"22222222-2222-4222-8222-222222222222", "repo:pr:9", "review"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
request = create.call_args.args[1]
|
|
128
|
+
remote_workspace.assert_called_once_with(
|
|
129
|
+
host="http://agent", api_key="session", working_dir=str(tmp_path)
|
|
130
|
+
)
|
|
131
|
+
assert request.workspace.working_dir == str(tmp_path)
|
|
132
|
+
conversation.close.assert_called_once_with()
|
|
133
|
+
|
|
134
|
+
|
|
103
135
|
def test_known_subject_resumes_once_per_delivery(monkeypatch):
|
|
104
136
|
state = {
|
|
105
137
|
_state_key("repo:pr:9"): {
|
|
@@ -388,8 +420,9 @@ def test_subjects_use_independent_kv_records(monkeypatch):
|
|
|
388
420
|
class _FakeCloud:
|
|
389
421
|
"""The OpenHands API's conversations, as the dispatcher uses them."""
|
|
390
422
|
|
|
391
|
-
def __init__(self, conversation=None):
|
|
423
|
+
def __init__(self, conversation=None, agent_profiles=None):
|
|
392
424
|
self.conversation = conversation
|
|
425
|
+
self.profiles = agent_profiles
|
|
393
426
|
self.started = []
|
|
394
427
|
self.sent = []
|
|
395
428
|
|
|
@@ -397,7 +430,14 @@ class _FakeCloud:
|
|
|
397
430
|
return self.conversation
|
|
398
431
|
|
|
399
432
|
def start(self, conversation_id, profile_id, title, prompt):
|
|
400
|
-
self.started.append(
|
|
433
|
+
self.started.append(
|
|
434
|
+
{"id": str(conversation_id), "profile": str(profile_id), "prompt": prompt}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def agent_profiles(self):
|
|
438
|
+
if self.profiles is None:
|
|
439
|
+
raise HTTPError("/api/agent-profiles", 403, "forbidden", {}, None)
|
|
440
|
+
return self.profiles
|
|
401
441
|
|
|
402
442
|
def send(self, conversation, prompt):
|
|
403
443
|
self.sent.append(prompt)
|
|
@@ -551,3 +591,130 @@ def test_cloud_run_stops_retrying_an_errored_delivery_after_the_bound(monkeypatc
|
|
|
551
591
|
# Assert
|
|
552
592
|
assert result["disposition"] == "deduplicated"
|
|
553
593
|
assert cloud.sent == []
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def test_cloud_prompt_names_the_replacement_conversation(monkeypatch):
|
|
597
|
+
"""A prompt that names its conversation names the one actually started."""
|
|
598
|
+
gone = "22222222-2222-4222-8222-222222222222"
|
|
599
|
+
state = {
|
|
600
|
+
_state_key("repo:pr:7"): {
|
|
601
|
+
"conversation_id": gone,
|
|
602
|
+
"delivery": "revision-1",
|
|
603
|
+
"head": "sha-1",
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
_fake_kv(monkeypatch, state)
|
|
607
|
+
cloud = _FakeCloud({"sandbox_status": "MISSING", "execution_status": None})
|
|
608
|
+
|
|
609
|
+
with _cloud_dispatcher(monkeypatch, cloud) as dispatcher:
|
|
610
|
+
result = dispatcher.deliver(
|
|
611
|
+
"repo:pr:7",
|
|
612
|
+
"revision-2",
|
|
613
|
+
lambda conversation_id: f"review as {conversation_id}",
|
|
614
|
+
head="sha-2",
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
[started] = cloud.started
|
|
618
|
+
assert started["prompt"] == f"review as {result['conversation_id']}"
|
|
619
|
+
assert result["conversation_id"] != gone
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def test_cloud_llm_provenance_reads_the_conversation_and_its_launch_profile(
|
|
623
|
+
monkeypatch,
|
|
624
|
+
):
|
|
625
|
+
launched = {
|
|
626
|
+
"agent_profile_id": "11111111-1111-4111-8111-111111111111",
|
|
627
|
+
"revision": 2,
|
|
628
|
+
}
|
|
629
|
+
profile = {
|
|
630
|
+
"id": launched["agent_profile_id"],
|
|
631
|
+
"name": "Code reviewer",
|
|
632
|
+
"revision": 2,
|
|
633
|
+
"llm_profile_ref": "review-sonnet",
|
|
634
|
+
}
|
|
635
|
+
conversation = {
|
|
636
|
+
"llm_model": "anthropic/claude-sonnet-4-5",
|
|
637
|
+
"launched_agent_profile": launched,
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
current = _cloud_dispatcher(monkeypatch, _FakeCloud(conversation, [profile]))
|
|
641
|
+
edited = _cloud_dispatcher(
|
|
642
|
+
monkeypatch, _FakeCloud(conversation, [{**profile, "revision": 3}])
|
|
643
|
+
)
|
|
644
|
+
unreadable = _cloud_dispatcher(monkeypatch, _FakeCloud(conversation, None))
|
|
645
|
+
gone = _cloud_dispatcher(monkeypatch, _FakeCloud(None, [profile]))
|
|
646
|
+
|
|
647
|
+
assert current.llm_provenance("conversation") == (
|
|
648
|
+
"review-sonnet",
|
|
649
|
+
"anthropic/claude-sonnet-4-5",
|
|
650
|
+
)
|
|
651
|
+
# An agent profile edited since launch, or one that cannot be read, cannot
|
|
652
|
+
# name the LLM profile the conversation started with; its model is known.
|
|
653
|
+
assert edited.llm_provenance("conversation") == (
|
|
654
|
+
"unknown",
|
|
655
|
+
"anthropic/claude-sonnet-4-5",
|
|
656
|
+
)
|
|
657
|
+
assert unreadable.llm_provenance("conversation") == (
|
|
658
|
+
"unknown",
|
|
659
|
+
"anthropic/claude-sonnet-4-5",
|
|
660
|
+
)
|
|
661
|
+
assert gone.llm_provenance("conversation") is None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def test_subject_conversation_is_this_automations_recorded_conversation(monkeypatch):
|
|
665
|
+
state = {
|
|
666
|
+
_state_key("repo:pr:7"): {
|
|
667
|
+
"conversation_id": "22222222-2222-4222-8222-222222222222",
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
_fake_kv(monkeypatch, state)
|
|
671
|
+
dispatcher = _dispatcher(monkeypatch)
|
|
672
|
+
|
|
673
|
+
assert (
|
|
674
|
+
dispatcher.subject_conversation("repo:pr:7")
|
|
675
|
+
== "22222222-2222-4222-8222-222222222222"
|
|
676
|
+
)
|
|
677
|
+
assert dispatcher.subject_conversation("repo:pr:8") is None
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@pytest.mark.parametrize("status", [404, 500])
|
|
681
|
+
def test_local_llm_provenance_of_a_missing_conversation(monkeypatch, status):
|
|
682
|
+
def urlopen(request, timeout):
|
|
683
|
+
assert request.full_url.endswith("/api/conversations/conversation")
|
|
684
|
+
assert request.get_header("X-session-api-key") == "session"
|
|
685
|
+
raise HTTPError(request.full_url, status, "error", {}, None)
|
|
686
|
+
|
|
687
|
+
monkeypatch.setattr(agent_conversation, "urlopen", urlopen)
|
|
688
|
+
dispatcher = _dispatcher(monkeypatch)
|
|
689
|
+
|
|
690
|
+
if status == 404:
|
|
691
|
+
# A deleted conversation names no LLM, so nothing is attributed to it.
|
|
692
|
+
assert dispatcher.llm_provenance("conversation") is None
|
|
693
|
+
else:
|
|
694
|
+
with pytest.raises(HTTPError):
|
|
695
|
+
dispatcher.llm_provenance("conversation")
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def test_cloud_error_retry_renders_the_prompt_for_its_conversation(monkeypatch):
|
|
699
|
+
"""A retried delivery still names the conversation it is sent to."""
|
|
700
|
+
current = "22222222-2222-4222-8222-222222222222"
|
|
701
|
+
state = {
|
|
702
|
+
_state_key("repo:pr:7"): {
|
|
703
|
+
"conversation_id": current,
|
|
704
|
+
"delivery": "revision-1",
|
|
705
|
+
"head": "sha-1",
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
_fake_kv(monkeypatch, state)
|
|
709
|
+
cloud = _FakeCloud({"sandbox_status": "RUNNING", "execution_status": "error"})
|
|
710
|
+
|
|
711
|
+
with _cloud_dispatcher(monkeypatch, cloud) as dispatcher:
|
|
712
|
+
result = dispatcher.deliver(
|
|
713
|
+
"repo:pr:7",
|
|
714
|
+
"revision-1",
|
|
715
|
+
lambda conversation_id: f"review as {conversation_id}",
|
|
716
|
+
head="sha-1",
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
assert result["disposition"] == "retried"
|
|
720
|
+
assert cloud.sent == [f"review as {current}"]
|