codee-agent 0.6.3__tar.gz → 0.6.5__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.
- {codee_agent-0.6.3 → codee_agent-0.6.5}/PKG-INFO +1 -1
- {codee_agent-0.6.3 → codee_agent-0.6.5}/pyproject.toml +1 -1
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/admin.py +143 -9
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/admin_service.py +32 -5
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/executor.py +12 -8
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_admin_service.py +60 -13
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_executor.py +84 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_abstract/provider.py +15 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_claude_code/provider.py +18 -2
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_claude_code/test.py +17 -2
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_codex/provider.py +30 -3
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_codex/test.py +9 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_github_copilot/test.py +25 -8
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_abstract/provider.py +29 -8
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/provider.py +13 -99
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/test.py +38 -97
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_jira/provider.py +24 -70
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_jira/test.py +49 -24
- {codee_agent-0.6.3 → codee_agent-0.6.5}/LICENSE +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/README.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/.gitignore +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/admin_api.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/init_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/mail_server.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/start_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_database/database.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.6.3 → codee_agent-0.6.5}/src/codee_tasks_jira/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codee-agent
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.5
|
|
4
4
|
Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
|
|
5
5
|
Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,codex,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
@@ -101,6 +101,11 @@ class ModelOption(BaseModel):
|
|
|
101
101
|
name: str
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
class ConversationMessage(BaseModel):
|
|
105
|
+
role: str
|
|
106
|
+
content: str
|
|
107
|
+
|
|
108
|
+
|
|
104
109
|
class ClaudeAccount(BaseModel):
|
|
105
110
|
"""One connected Claude account as the settings page lists it.
|
|
106
111
|
|
|
@@ -356,6 +361,12 @@ class AdminState(rx.State):
|
|
|
356
361
|
tasks_provider: str = "jira"
|
|
357
362
|
coding_agent: str = "claude_code"
|
|
358
363
|
max_parallel_agents: str = "3"
|
|
364
|
+
agent_test_open: bool = False
|
|
365
|
+
agent_test_input: str = ""
|
|
366
|
+
agent_test_session_id: str = ""
|
|
367
|
+
agent_test_messages: list[ConversationMessage] = []
|
|
368
|
+
agent_test_sending: bool = False
|
|
369
|
+
agent_test_generation: int = 0
|
|
359
370
|
# Whether the executor runs Claude Code on the connected accounts instead
|
|
360
371
|
# of leaving ~/.claude/.credentials.json alone.
|
|
361
372
|
claude_code_rotate_keys: bool = False
|
|
@@ -1516,6 +1527,61 @@ class AdminState(rx.State):
|
|
|
1516
1527
|
error = self._persist_settings()
|
|
1517
1528
|
return rx.toast.error(error) if error else rx.toast.success("Settings saved")
|
|
1518
1529
|
|
|
1530
|
+
def open_agent_test(self) -> None:
|
|
1531
|
+
self.agent_test_input = ""
|
|
1532
|
+
self.agent_test_session_id = ""
|
|
1533
|
+
self.agent_test_messages = []
|
|
1534
|
+
self.agent_test_sending = False
|
|
1535
|
+
self.agent_test_generation += 1
|
|
1536
|
+
self.agent_test_open = True
|
|
1537
|
+
|
|
1538
|
+
def set_agent_test_open(self, open_: bool) -> None:
|
|
1539
|
+
self.agent_test_open = open_
|
|
1540
|
+
if not open_:
|
|
1541
|
+
self.agent_test_generation += 1
|
|
1542
|
+
self.agent_test_sending = False
|
|
1543
|
+
|
|
1544
|
+
def set_agent_test_input(self, value: str) -> None:
|
|
1545
|
+
self.agent_test_input = value
|
|
1546
|
+
|
|
1547
|
+
@rx.event(background=True)
|
|
1548
|
+
async def send_agent_test_message(self) -> Any:
|
|
1549
|
+
async with self:
|
|
1550
|
+
message = self.agent_test_input.strip()
|
|
1551
|
+
if not message or self.agent_test_sending:
|
|
1552
|
+
return
|
|
1553
|
+
agent = self.coding_agent
|
|
1554
|
+
session_id = self.agent_test_session_id
|
|
1555
|
+
generation = self.agent_test_generation
|
|
1556
|
+
self.agent_test_input = ""
|
|
1557
|
+
self.agent_test_sending = True
|
|
1558
|
+
self.agent_test_messages = [
|
|
1559
|
+
*self.agent_test_messages,
|
|
1560
|
+
ConversationMessage(role="user", content=message),
|
|
1561
|
+
]
|
|
1562
|
+
try:
|
|
1563
|
+
response, session_id = await asyncio.to_thread(
|
|
1564
|
+
SERVICE.test_agent_conversation,
|
|
1565
|
+
agent,
|
|
1566
|
+
message,
|
|
1567
|
+
session_id,
|
|
1568
|
+
)
|
|
1569
|
+
except Exception as error:
|
|
1570
|
+
async with self:
|
|
1571
|
+
if self.agent_test_generation == generation:
|
|
1572
|
+
self.agent_test_sending = False
|
|
1573
|
+
yield rx.toast.error(f"The coding agent failed: {error}")
|
|
1574
|
+
return
|
|
1575
|
+
async with self:
|
|
1576
|
+
if self.agent_test_generation != generation:
|
|
1577
|
+
return
|
|
1578
|
+
self.agent_test_session_id = session_id
|
|
1579
|
+
self.agent_test_messages = [
|
|
1580
|
+
*self.agent_test_messages,
|
|
1581
|
+
ConversationMessage(role="assistant", content=response),
|
|
1582
|
+
]
|
|
1583
|
+
self.agent_test_sending = False
|
|
1584
|
+
|
|
1519
1585
|
|
|
1520
1586
|
ACCENT = "var(--codee-accent)"
|
|
1521
1587
|
ACCENT_DEEP = "var(--codee-accent-deep)"
|
|
@@ -2402,9 +2468,9 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2402
2468
|
rx.vstack(rx.hstack(rx.text(run.skill_name, font_weight="600"),
|
|
2403
2469
|
rx.badge(run.status, color_scheme=rx.cond(run.status == "succeeded", "green", "red"))),
|
|
2404
2470
|
rx.text(local_datetime(run.started_at), color=MUTED,
|
|
2405
|
-
|
|
2471
|
+
font_size="0.8rem",
|
|
2406
2472
|
font_family="IBM Plex Mono, monospace"),
|
|
2407
|
-
|
|
2473
|
+
rx.text("Thread ID: ", run.session_id, color=MUTED,
|
|
2408
2474
|
font_size="0.8rem",
|
|
2409
2475
|
font_family="IBM Plex Mono, monospace"),
|
|
2410
2476
|
rx.text(run.preview, color=MUTED),
|
|
@@ -2418,9 +2484,10 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2418
2484
|
rx.cond((run.user_message != "") | (run.response != ""), rx.accordion.root(rx.accordion.item(
|
|
2419
2485
|
header="Run info", content=rx.vstack(
|
|
2420
2486
|
rx.text("User message", font_weight="600"),
|
|
2421
|
-
|
|
2487
|
+
rx.text(run.user_message, white_space="pre-wrap"),
|
|
2422
2488
|
rx.cond(run.response != "", rx.fragment(
|
|
2423
|
-
rx.text("LLM response", font_weight="600",
|
|
2489
|
+
rx.text("LLM response", font_weight="600",
|
|
2490
|
+
margin_top="0.75rem"),
|
|
2424
2491
|
rx.text(run.response, white_space="pre-wrap"))),
|
|
2425
2492
|
spacing="2", align="start", width="100%"), value=run.started_at),
|
|
2426
2493
|
collapsible=True, width="100%")),
|
|
@@ -2644,7 +2711,8 @@ def workflow_page() -> rx.Component:
|
|
|
2644
2711
|
rx.center(
|
|
2645
2712
|
rx.vstack(
|
|
2646
2713
|
rx.spinner(size="3"),
|
|
2647
|
-
rx.foreach(AdminState.workflow_progress,
|
|
2714
|
+
rx.foreach(AdminState.workflow_progress,
|
|
2715
|
+
workflow_progress_line),
|
|
2648
2716
|
spacing="3",
|
|
2649
2717
|
align="center",
|
|
2650
2718
|
width="100%",
|
|
@@ -3221,6 +3289,64 @@ def claude_code_setting() -> rx.Component:
|
|
|
3221
3289
|
padding="1.25rem", background=SURFACE, border=BORDER, width="100%")
|
|
3222
3290
|
|
|
3223
3291
|
|
|
3292
|
+
def conversation_message(message: ConversationMessage) -> rx.Component:
|
|
3293
|
+
is_user = message.role == "user"
|
|
3294
|
+
return rx.box(
|
|
3295
|
+
rx.text(message.content, white_space="pre-wrap"),
|
|
3296
|
+
align_self=rx.cond(is_user, "end", "start"),
|
|
3297
|
+
background=rx.cond(is_user, ACTIVE, SURFACE),
|
|
3298
|
+
border=rx.cond(is_user, "none", BORDER),
|
|
3299
|
+
border_radius="6px",
|
|
3300
|
+
padding="0.65rem 0.8rem",
|
|
3301
|
+
max_width="85%",
|
|
3302
|
+
)
|
|
3303
|
+
|
|
3304
|
+
|
|
3305
|
+
def agent_test_dialog() -> rx.Component:
|
|
3306
|
+
return rx.dialog.root(
|
|
3307
|
+
rx.dialog.content(
|
|
3308
|
+
rx.dialog.title("Test conversation"),
|
|
3309
|
+
rx.dialog.description(
|
|
3310
|
+
"Chat with the selected coding agent in the Codee project.",
|
|
3311
|
+
color=MUTED),
|
|
3312
|
+
rx.scroll_area(
|
|
3313
|
+
rx.vstack(
|
|
3314
|
+
rx.cond(
|
|
3315
|
+
AdminState.agent_test_messages.length() == 0,
|
|
3316
|
+
rx.text("Send a message to start the conversation.",
|
|
3317
|
+
color=MUTED, font_size="0.9rem",
|
|
3318
|
+
align_self="center", margin_top="3rem"),
|
|
3319
|
+
rx.foreach(AdminState.agent_test_messages,
|
|
3320
|
+
conversation_message)),
|
|
3321
|
+
width="100%", spacing="3"),
|
|
3322
|
+
type="auto", scrollbars="vertical", height="22rem",
|
|
3323
|
+
width="100%", margin_top="1rem"),
|
|
3324
|
+
rx.form(
|
|
3325
|
+
rx.hstack(
|
|
3326
|
+
rx.input(
|
|
3327
|
+
value=AdminState.agent_test_input,
|
|
3328
|
+
on_change=AdminState.set_agent_test_input,
|
|
3329
|
+
placeholder="Message the agent",
|
|
3330
|
+
disabled=AdminState.agent_test_sending,
|
|
3331
|
+
auto_focus=True,
|
|
3332
|
+
width="100%"),
|
|
3333
|
+
rx.button(rx.icon("send", size=16), type="submit",
|
|
3334
|
+
loading=AdminState.agent_test_sending,
|
|
3335
|
+
disabled=AdminState.agent_test_input == ""),
|
|
3336
|
+
spacing="2", width="100%"),
|
|
3337
|
+
on_submit=AdminState.send_agent_test_message,
|
|
3338
|
+
reset_on_submit=False,
|
|
3339
|
+
width="100%", margin_top="1rem"),
|
|
3340
|
+
rx.flex(
|
|
3341
|
+
rx.dialog.close(rx.button("Close", variant="soft",
|
|
3342
|
+
color_scheme="gray")),
|
|
3343
|
+
justify="end", margin_top="1rem"),
|
|
3344
|
+
max_width="38rem"),
|
|
3345
|
+
open=AdminState.agent_test_open,
|
|
3346
|
+
on_open_change=AdminState.set_agent_test_open,
|
|
3347
|
+
)
|
|
3348
|
+
|
|
3349
|
+
|
|
3224
3350
|
def settings_page() -> rx.Component:
|
|
3225
3351
|
jira = TasksProvider.JIRA
|
|
3226
3352
|
jira_fields = rx.vstack(
|
|
@@ -3248,10 +3374,17 @@ def settings_page() -> rx.Component:
|
|
|
3248
3374
|
rx.heading("Coding agent", size="4", margin_bottom="1rem"),
|
|
3249
3375
|
rx.vstack(
|
|
3250
3376
|
field("Default agent",
|
|
3251
|
-
rx.
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3377
|
+
rx.hstack(
|
|
3378
|
+
rx.select(
|
|
3379
|
+
["claude_code", "github_copilot", "codex"],
|
|
3380
|
+
value=AdminState.coding_agent,
|
|
3381
|
+
on_change=AdminState.set_coding_agent,
|
|
3382
|
+
width="100%"),
|
|
3383
|
+
rx.button(rx.icon("messages-square", size=16),
|
|
3384
|
+
"Test conversation", variant="outline",
|
|
3385
|
+
white_space="nowrap",
|
|
3386
|
+
on_click=AdminState.open_agent_test),
|
|
3387
|
+
width="100%", spacing="3")),
|
|
3255
3388
|
field("Max parallel tasks",
|
|
3256
3389
|
rx.input(value=AdminState.max_parallel_agents,
|
|
3257
3390
|
on_change=AdminState.set_max_parallel_agents,
|
|
@@ -3277,6 +3410,7 @@ def settings_page() -> rx.Component:
|
|
|
3277
3410
|
padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
|
|
3278
3411
|
rx.button(rx.icon("save", size=16), "Save settings",
|
|
3279
3412
|
on_click=AdminState.save_settings),
|
|
3413
|
+
agent_test_dialog(),
|
|
3280
3414
|
spacing="5", align="start", width="100%"))
|
|
3281
3415
|
|
|
3282
3416
|
|
|
@@ -192,6 +192,7 @@ class WorkflowGeneration:
|
|
|
192
192
|
workflow: dict[str, Any] | None = None
|
|
193
193
|
error: str = ""
|
|
194
194
|
|
|
195
|
+
|
|
195
196
|
# The checks the settings page runs against the tasks provider, in the order it
|
|
196
197
|
# shows them: the second is only worth attempting once the first passes.
|
|
197
198
|
TASKS_CHECK = "Tasks can be pulled"
|
|
@@ -893,7 +894,8 @@ class AdminService:
|
|
|
893
894
|
# and how long it stands. Shared by every visitor to the dashboard,
|
|
894
895
|
# because it is a property of the accounts rather than of whoever is
|
|
895
896
|
# looking at them.
|
|
896
|
-
self._usage_cache: dict[int,
|
|
897
|
+
self._usage_cache: dict[int,
|
|
898
|
+
tuple[ConnectedAccount, float, float]] = {}
|
|
897
899
|
self._usage_lock = threading.Lock()
|
|
898
900
|
|
|
899
901
|
def _git_push(self, message: str) -> tuple[bool, str]:
|
|
@@ -1221,7 +1223,8 @@ class AdminService:
|
|
|
1221
1223
|
name belongs on the other item's graph, so it is kept out of this one.
|
|
1222
1224
|
"""
|
|
1223
1225
|
if not skills:
|
|
1224
|
-
report(
|
|
1226
|
+
report(
|
|
1227
|
+
f"No issue-trigger skills for work item {issue_type.capitalize()}.")
|
|
1225
1228
|
return {"nodes": [], "edges": [], "warnings": []}
|
|
1226
1229
|
|
|
1227
1230
|
documents = []
|
|
@@ -1414,7 +1417,8 @@ class AdminService:
|
|
|
1414
1417
|
for status in (transition["source"], transition["target"])}
|
|
1415
1418
|
statuses, foreign_statuses = _own_work_item_statuses(
|
|
1416
1419
|
statuses, scope, triggered | moved,
|
|
1417
|
-
dict.fromkeys(document for _,
|
|
1420
|
+
dict.fromkeys(document for _,
|
|
1421
|
+
document in skill_documents.values()),
|
|
1418
1422
|
)
|
|
1419
1423
|
if foreign_statuses:
|
|
1420
1424
|
report(
|
|
@@ -1831,7 +1835,7 @@ class AdminService:
|
|
|
1831
1835
|
existing, _ = parse_skill(current_path.read_text())
|
|
1832
1836
|
extra = {key: value for key, value in existing.items()
|
|
1833
1837
|
if key not in MANAGED}
|
|
1834
|
-
action =f"rename {old_slug} -> {name}" if name != old_slug else f"update {name}"
|
|
1838
|
+
action = f"rename {old_slug} -> {name}" if name != old_slug else f"update {name}"
|
|
1835
1839
|
saved, pushed, message = self._write_and_push(
|
|
1836
1840
|
current_path,
|
|
1837
1841
|
build_skill(frontmatter, extra, skill["body"]),
|
|
@@ -2439,6 +2443,28 @@ class AdminService:
|
|
|
2439
2443
|
except ValueError as exc:
|
|
2440
2444
|
raise RuntimeError(str(exc)) from exc
|
|
2441
2445
|
|
|
2446
|
+
def test_agent_conversation(
|
|
2447
|
+
self, agent_code: str, user_message: str, session_id: str = ""
|
|
2448
|
+
) -> tuple[str, str]:
|
|
2449
|
+
"""Run one turn with the selected agent and return reply plus session id."""
|
|
2450
|
+
selected = resolve_agent_code(agent_code)
|
|
2451
|
+
if selected is None:
|
|
2452
|
+
raise RuntimeError(f"Unsupported coding agent: {agent_code}")
|
|
2453
|
+
agent = build_coding_agent(self.context.settings, self.root, selected)
|
|
2454
|
+
actual_session_id = session_id or str(uuid.uuid4())
|
|
2455
|
+
|
|
2456
|
+
def opened(opened_session_id: str) -> None:
|
|
2457
|
+
nonlocal actual_session_id
|
|
2458
|
+
actual_session_id = opened_session_id
|
|
2459
|
+
|
|
2460
|
+
if session_id:
|
|
2461
|
+
response = agent.continue_conversation(
|
|
2462
|
+
user_message, session_id, on_session_id=opened)
|
|
2463
|
+
else:
|
|
2464
|
+
response = agent.run(
|
|
2465
|
+
user_message, actual_session_id, on_session_id=opened)
|
|
2466
|
+
return response, actual_session_id
|
|
2467
|
+
|
|
2442
2468
|
def setup_tasks_mcp(
|
|
2443
2469
|
self,
|
|
2444
2470
|
tasks_provider: str,
|
|
@@ -2600,7 +2626,8 @@ def _format_token_expiry(expires_at: str | None) -> str:
|
|
|
2600
2626
|
return "refreshes on next check"
|
|
2601
2627
|
if deadline.tzinfo is None:
|
|
2602
2628
|
deadline = deadline.replace(tzinfo=timezone.utc)
|
|
2603
|
-
minutes = int(
|
|
2629
|
+
minutes = int(
|
|
2630
|
+
(deadline - datetime.now(timezone.utc)).total_seconds() // 60)
|
|
2604
2631
|
if minutes < 1:
|
|
2605
2632
|
return "refreshes on next check"
|
|
2606
2633
|
if minutes < 60:
|
|
@@ -11,8 +11,8 @@ from codee_agent_claude_code.provider import ClaudeCodeAgent
|
|
|
11
11
|
from codee_agent_codex.provider import CodexAgent
|
|
12
12
|
from codee_agent_github_copilot.provider import GitHubCopilotAgent
|
|
13
13
|
from codee_main_context.context import (
|
|
14
|
-
CodeeMainContext, CodingAgent,
|
|
15
|
-
|
|
14
|
+
CodeeMainContext, CodingAgent, Settings, codee_issue_types, data_dir,
|
|
15
|
+
load_settings, project_root)
|
|
16
16
|
from codee_main_context.logging import configure_logging, get_logger
|
|
17
17
|
from codee_tasks_abstract.provider import AbstractTasksProvider
|
|
18
18
|
|
|
@@ -424,12 +424,16 @@ def run_once() -> None:
|
|
|
424
424
|
log.info("Incoming %s (%s, %s, %s): %s",
|
|
425
425
|
task_id, status, issue_type, priority, summary)
|
|
426
426
|
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
427
|
+
# An item whose parent is one Codee polls in its own right is left
|
|
428
|
+
# alone: the parent's own run is what decides what its children need,
|
|
429
|
+
# and picking the child up here as well is two agents on one change.
|
|
430
|
+
# Its type is what says so — a work item selected by a query of the
|
|
431
|
+
# user's own shields nothing, since nothing in a parent says whether
|
|
432
|
+
# that condition would have claimed it.
|
|
433
|
+
if task.is_parent_codee_work_item:
|
|
434
|
+
log.debug("Skipping %s: its parent %s is a %s, which Codee polls "
|
|
435
|
+
"in its own right",
|
|
436
|
+
task_id, task.parent.key, task.parent.work_item_type)
|
|
433
437
|
continue
|
|
434
438
|
|
|
435
439
|
skill = match_issue_skill(issue_skills, status, issue_type)
|
|
@@ -80,7 +80,8 @@ class NormalizeWorkItemsTest(unittest.TestCase):
|
|
|
80
80
|
[("story", ["Story"], ""), ("task", ["Task", "Bug"], "")])
|
|
81
81
|
|
|
82
82
|
self.assertEqual(error, "")
|
|
83
|
-
self.assertEqual(
|
|
83
|
+
self.assertEqual(
|
|
84
|
+
mapping, {"story": ["Story"], "task": ["Task", "Bug"]})
|
|
84
85
|
|
|
85
86
|
def test_a_type_listed_twice_in_one_row_is_kept_once(self) -> None:
|
|
86
87
|
mapping, _, error = normalize_work_items(
|
|
@@ -164,6 +165,42 @@ class NormalizeWorkItemsTest(unittest.TestCase):
|
|
|
164
165
|
self.assertEqual(error, "'bug' is listed twice")
|
|
165
166
|
|
|
166
167
|
|
|
168
|
+
class TestAgentConversationTest(unittest.TestCase):
|
|
169
|
+
def setUp(self) -> None:
|
|
170
|
+
self.service = AdminService.__new__(AdminService)
|
|
171
|
+
self.service.root = Path("/repo")
|
|
172
|
+
self.service.context = Mock(settings=Settings())
|
|
173
|
+
|
|
174
|
+
@patch("codee.admin_service.build_coding_agent")
|
|
175
|
+
def test_first_turn_returns_the_agent_session_id(self, build_agent) -> None:
|
|
176
|
+
agent = build_agent.return_value
|
|
177
|
+
|
|
178
|
+
def run(message, session_id, model="", on_session_id=None):
|
|
179
|
+
on_session_id("agent-thread")
|
|
180
|
+
return "Final response"
|
|
181
|
+
|
|
182
|
+
agent.run.side_effect = run
|
|
183
|
+
|
|
184
|
+
response, session_id = self.service.test_agent_conversation(
|
|
185
|
+
"codex", "Hello")
|
|
186
|
+
|
|
187
|
+
self.assertEqual(response, "Final response")
|
|
188
|
+
self.assertEqual(session_id, "agent-thread")
|
|
189
|
+
build_agent.assert_called_once_with(
|
|
190
|
+
self.service.context.settings, Path("/repo"), CodingAgent.CODEX)
|
|
191
|
+
|
|
192
|
+
@patch("codee.admin_service.build_coding_agent")
|
|
193
|
+
def test_later_turn_resumes_the_same_session(self, build_agent) -> None:
|
|
194
|
+
build_agent.return_value.continue_conversation.return_value = "Still here"
|
|
195
|
+
|
|
196
|
+
response, session_id = self.service.test_agent_conversation(
|
|
197
|
+
"github_copilot", "What did I ask?", "same-thread")
|
|
198
|
+
|
|
199
|
+
self.assertEqual((response, session_id), ("Still here", "same-thread"))
|
|
200
|
+
call = build_agent.return_value.continue_conversation.call_args
|
|
201
|
+
self.assertEqual(call.args[:2], ("What did I ask?", "same-thread"))
|
|
202
|
+
|
|
203
|
+
|
|
167
204
|
class AdminServiceWorkItemsTest(unittest.TestCase):
|
|
168
205
|
def _service(self, directory: Path) -> AdminService:
|
|
169
206
|
service = AdminService.__new__(AdminService)
|
|
@@ -408,7 +445,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
408
445
|
self._connect("one@example.com")
|
|
409
446
|
self._connect("two@example.com")
|
|
410
447
|
second = self.service.claude_code_accounts()[1]
|
|
411
|
-
claude_code_accounts.set_current_account(
|
|
448
|
+
claude_code_accounts.set_current_account(
|
|
449
|
+
second.id, self.service.context)
|
|
412
450
|
|
|
413
451
|
self.service.disconnect_claude_code_account(second.id)
|
|
414
452
|
|
|
@@ -423,7 +461,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
423
461
|
|
|
424
462
|
self.service.disconnect_claude_code_account(account.id)
|
|
425
463
|
|
|
426
|
-
self.assertEqual(claude_code_accounts.accounts(
|
|
464
|
+
self.assertEqual(claude_code_accounts.accounts(
|
|
465
|
+
self.service.context), [])
|
|
427
466
|
|
|
428
467
|
def test_switching_rotation_off_forgets_which_account_is_in_use(self) -> None:
|
|
429
468
|
# So switching it back on later starts from the top rather than from
|
|
@@ -431,7 +470,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
431
470
|
self._connect("one@example.com")
|
|
432
471
|
self._connect("two@example.com")
|
|
433
472
|
second = self.service.claude_code_accounts()[1]
|
|
434
|
-
claude_code_accounts.set_current_account(
|
|
473
|
+
claude_code_accounts.set_current_account(
|
|
474
|
+
second.id, self.service.context)
|
|
435
475
|
|
|
436
476
|
self.service.save_settings("jira", "claude_code", 3, {}, None, None,
|
|
437
477
|
"", False)
|
|
@@ -465,14 +505,16 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
465
505
|
"access-1", "refresh-1", expires_at=far_future,
|
|
466
506
|
refresh_expires_at=far_future))
|
|
467
507
|
|
|
468
|
-
self.assertFalse(self.service.claude_code_accounts()
|
|
508
|
+
self.assertFalse(self.service.claude_code_accounts()
|
|
509
|
+
[0].needs_reconnect)
|
|
469
510
|
|
|
470
511
|
def test_an_account_with_no_known_window_is_not_flagged(self) -> None:
|
|
471
512
|
# Zero means the API never said, which is not the same as expired.
|
|
472
513
|
self._connect("one@example.com", tokens=claude_oauth.Tokens(
|
|
473
514
|
"access-1", "refresh-1", expires_at=1, refresh_expires_at=0))
|
|
474
515
|
|
|
475
|
-
self.assertFalse(self.service.claude_code_accounts()
|
|
516
|
+
self.assertFalse(self.service.claude_code_accounts()
|
|
517
|
+
[0].needs_reconnect)
|
|
476
518
|
|
|
477
519
|
def test_each_account_reports_both_of_its_windows(self) -> None:
|
|
478
520
|
self._connect("one@example.com")
|
|
@@ -487,7 +529,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
487
529
|
|
|
488
530
|
self.assertEqual(measured[0].session_percent, 12.0)
|
|
489
531
|
self.assertEqual(measured[0].weekly_percent, 70.0)
|
|
490
|
-
self.assertEqual(measured[0].weekly_resets,
|
|
532
|
+
self.assertEqual(measured[0].weekly_resets,
|
|
533
|
+
"2026-09-16T09:00:00+00:00")
|
|
491
534
|
self.assertEqual(measured[0].usage_error, "")
|
|
492
535
|
|
|
493
536
|
def test_the_reading_is_cached_rather_than_taken_every_redraw(self) -> None:
|
|
@@ -555,7 +598,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
555
598
|
measured[1].id, self.service.context)
|
|
556
599
|
again = self.service.claude_code_account_usage()
|
|
557
600
|
|
|
558
|
-
self.assertEqual(
|
|
601
|
+
self.assertEqual(
|
|
602
|
+
[account.in_use for account in measured], [True, False])
|
|
559
603
|
self.assertEqual([account.in_use for account in again], [False, True])
|
|
560
604
|
self.assertEqual(again[1].session_percent, 3.0)
|
|
561
605
|
|
|
@@ -594,7 +638,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
594
638
|
|
|
595
639
|
self.assertEqual(measured[0].session_percent, 12.0)
|
|
596
640
|
self.assertEqual(measured[0].weekly_percent, 70.0)
|
|
597
|
-
self.assertEqual(measured[0].session_resets,
|
|
641
|
+
self.assertEqual(measured[0].session_resets,
|
|
642
|
+
"2026-09-13T14:10:00+00:00")
|
|
598
643
|
self.assertEqual(measured[0].usage_error, "")
|
|
599
644
|
|
|
600
645
|
def test_a_rate_limit_with_nothing_to_fall_back_on_says_so(self) -> None:
|
|
@@ -662,7 +707,8 @@ class AdminServiceClaudeCodeAccountsTest(unittest.TestCase):
|
|
|
662
707
|
return_value=Usage(limited=False, windows={}, resets_at={})):
|
|
663
708
|
measured = self.service.claude_code_account_usage()
|
|
664
709
|
|
|
665
|
-
self.assertEqual(
|
|
710
|
+
self.assertEqual(
|
|
711
|
+
[account.in_use for account in measured], [True, False])
|
|
666
712
|
|
|
667
713
|
def test_no_accounts_means_no_requests(self) -> None:
|
|
668
714
|
with patch("codee.admin_service.fetch_usage") as fetch:
|
|
@@ -1927,7 +1973,6 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1927
1973
|
|
|
1928
1974
|
self.assertEqual(generate.call_count, 1)
|
|
1929
1975
|
|
|
1930
|
-
|
|
1931
1976
|
def test_generate_workflow_regenerates_a_graph_from_an_older_version(self) -> None:
|
|
1932
1977
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
1933
1978
|
root = Path(temporary_directory)
|
|
@@ -2046,7 +2091,8 @@ class AdminServiceWorkflowAgentTest(unittest.TestCase):
|
|
|
2046
2091
|
|
|
2047
2092
|
nodes = self._story_nodes(service)
|
|
2048
2093
|
|
|
2049
|
-
self.assertNotIn("workflow-node--agent",
|
|
2094
|
+
self.assertNotIn("workflow-node--agent",
|
|
2095
|
+
nodes["Review"]["className"])
|
|
2050
2096
|
self.assertNotIn("style", nodes["Review"])
|
|
2051
2097
|
|
|
2052
2098
|
def test_changing_the_agent_needs_no_second_inference(self) -> None:
|
|
@@ -2491,7 +2537,8 @@ class AdminServiceAgentModelsTest(unittest.TestCase):
|
|
|
2491
2537
|
patch.object(ClaudeCodeAgent, "list_models") as claude_models:
|
|
2492
2538
|
models = service.list_agent_models("codex")
|
|
2493
2539
|
|
|
2494
|
-
self.assertEqual(
|
|
2540
|
+
self.assertEqual(
|
|
2541
|
+
models, [{"id": "gpt-6-astra", "name": "GPT-6 Astra"}])
|
|
2495
2542
|
claude_models.assert_not_called()
|
|
2496
2543
|
|
|
2497
2544
|
def test_an_agent_codee_cannot_run_falls_back_to_the_default(self) -> None:
|
|
@@ -12,8 +12,11 @@ from codee_main_context.context import (
|
|
|
12
12
|
from codee_tasks_azure_devops.provider import AzureDevOpsTasksProvider
|
|
13
13
|
from codee_tasks_jira.provider import JiraTasksProvider
|
|
14
14
|
|
|
15
|
+
from codee_tasks_abstract.provider import Task
|
|
16
|
+
|
|
15
17
|
from codee import executor, tasks_providers
|
|
16
18
|
from codee.lib import runs_db
|
|
19
|
+
from codee.lib.trigger_issue_skills import IssueTriggeredSkill
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class RefreshConfigTest(unittest.TestCase):
|
|
@@ -414,3 +417,84 @@ class _Exploding:
|
|
|
414
417
|
|
|
415
418
|
if __name__ == "__main__":
|
|
416
419
|
unittest.main()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _polled_task(key: str, issue_type: str = "task",
|
|
423
|
+
parent_type: str | None = None) -> Task:
|
|
424
|
+
"""One task as a provider hands it over, optionally under a parent.
|
|
425
|
+
|
|
426
|
+
``codee_work_item_types`` is what every provider fills in from the type
|
|
427
|
+
rows in Settings; here it stands for the default mapping, `story` pointed
|
|
428
|
+
at "Story" and `task` at "Task".
|
|
429
|
+
"""
|
|
430
|
+
parent = None
|
|
431
|
+
if parent_type is not None:
|
|
432
|
+
parent = Task(key=f"{key}-parent", summary="The parent",
|
|
433
|
+
status="Active", issue_type=parent_type.casefold(),
|
|
434
|
+
priority="High", work_item_type=parent_type)
|
|
435
|
+
return Task(key=key, summary="Some work", status="Ready",
|
|
436
|
+
issue_type=issue_type, priority="High",
|
|
437
|
+
work_item_type=issue_type.capitalize(), parent=parent,
|
|
438
|
+
codee_work_item_types=frozenset({"story", "task"}))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class ParentWorkItemSkipTest(unittest.TestCase):
|
|
442
|
+
"""A poll leaves an item whose parent Codee polls to that parent's run."""
|
|
443
|
+
|
|
444
|
+
def setUp(self) -> None:
|
|
445
|
+
self.provider = Mock()
|
|
446
|
+
self.provider.is_configured.return_value = True
|
|
447
|
+
self.skills = [
|
|
448
|
+
IssueTriggeredSkill(name="Task developer", slug="task-developer",
|
|
449
|
+
path=Path("/repo/.claude/skills/task/SKILL.md"),
|
|
450
|
+
statuses=("Ready",), issue_type="task"),
|
|
451
|
+
IssueTriggeredSkill(name="Story developer", slug="story-developer",
|
|
452
|
+
path=Path("/repo/.claude/skills/story/SKILL.md"),
|
|
453
|
+
statuses=("Ready",), issue_type="story"),
|
|
454
|
+
]
|
|
455
|
+
for name in ("trigger_cron_skills", "trigger_aws_sqs_skills",
|
|
456
|
+
"trigger_email_skills", "_refresh_config"):
|
|
457
|
+
patcher = patch.object(executor, name)
|
|
458
|
+
self.addCleanup(patcher.stop)
|
|
459
|
+
patcher.start()
|
|
460
|
+
for name, result in (("_pull_latest_code", True),
|
|
461
|
+
("_load_sessions", {}),
|
|
462
|
+
("find_issue_triggered_skills", self.skills)):
|
|
463
|
+
patcher = patch.object(executor, name, return_value=result)
|
|
464
|
+
self.addCleanup(patcher.stop)
|
|
465
|
+
patcher.start()
|
|
466
|
+
provider = patch.object(executor, "tasks_provider", self.provider)
|
|
467
|
+
self.addCleanup(provider.stop)
|
|
468
|
+
provider.start()
|
|
469
|
+
|
|
470
|
+
def _submitted(self, *tasks: Task) -> list[str]:
|
|
471
|
+
"""The task ids a poll over ``tasks`` actually launched an agent for."""
|
|
472
|
+
self.provider.get_tasks.return_value = list(tasks)
|
|
473
|
+
with patch.object(executor, "_submit_task") as submit:
|
|
474
|
+
executor.run_once()
|
|
475
|
+
return [call.args[0] for call in submit.call_args_list]
|
|
476
|
+
|
|
477
|
+
def test_an_item_under_a_mapped_parent_is_left_to_that_parents_run(self) -> None:
|
|
478
|
+
self.assertEqual(
|
|
479
|
+
self._submitted(_polled_task("NIM-1", parent_type="Story")), [])
|
|
480
|
+
|
|
481
|
+
def test_a_parent_mapped_to_any_work_item_counts(self) -> None:
|
|
482
|
+
# Not only the story: a task under a task is worked by the parent's
|
|
483
|
+
# own run just the same.
|
|
484
|
+
self.assertEqual(
|
|
485
|
+
self._submitted(_polled_task("NIM-2", parent_type="Task")), [])
|
|
486
|
+
|
|
487
|
+
def test_a_story_under_a_mapped_parent_is_skipped_too(self) -> None:
|
|
488
|
+
# The rule is about the parent, not about what the child is.
|
|
489
|
+
self.assertEqual(
|
|
490
|
+
self._submitted(_polled_task("NIM-3", issue_type="story",
|
|
491
|
+
parent_type="Story")), [])
|
|
492
|
+
|
|
493
|
+
def test_an_item_under_an_unmapped_parent_is_worked(self) -> None:
|
|
494
|
+
# An Epic is nothing Codee polls, so nothing else will do this work.
|
|
495
|
+
self.assertEqual(
|
|
496
|
+
self._submitted(_polled_task("NIM-4", parent_type="Epic")),
|
|
497
|
+
["NIM-4"])
|
|
498
|
+
|
|
499
|
+
def test_an_item_with_no_parent_is_worked(self) -> None:
|
|
500
|
+
self.assertEqual(self._submitted(_polled_task("NIM-5")), ["NIM-5"])
|
|
@@ -69,6 +69,21 @@ class AbstractCodingAgent(ABC):
|
|
|
69
69
|
"""
|
|
70
70
|
...
|
|
71
71
|
|
|
72
|
+
def continue_conversation(
|
|
73
|
+
self,
|
|
74
|
+
user_message: str,
|
|
75
|
+
session_id: str,
|
|
76
|
+
model: str = "",
|
|
77
|
+
on_session_id: Callable[[str], None] | None = None,
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Continue an existing agent conversation and return its final reply.
|
|
80
|
+
|
|
81
|
+
Agents whose normal run command resumes when given an existing session
|
|
82
|
+
need no special implementation. Agents with a distinct resume command
|
|
83
|
+
override this method.
|
|
84
|
+
"""
|
|
85
|
+
return self.run(user_message, session_id, model, on_session_id)
|
|
86
|
+
|
|
72
87
|
def skill_prompt(self, slug: str, path: Path, argument: str = "",
|
|
73
88
|
argument_name: str = "") -> str:
|
|
74
89
|
"""The message that makes this agent run the skill stored at ``path``.
|