Skip to content

Commit ee01b34

Browse files
authored
corp-ship combat wake (#416)
* fix: stop start_task silently auto-steering on a busy ship `start_task` on a ship with an occupied task slot used to route the new request into the running task's `_steer_existing_task` path and return `success: true, steered: true`. Two failure modes followed: - Distinct follow-up intents (e.g. "buy a probe and an Atlas" on the single personal-ship slot) collapsed into a steer of an unrelated in-flight task. The task agent had no obligation to act on the steer; the new request was silently dropped while the voice agent reported success to the commander. - The shared task_id in the response caused the LLM to double-count a single `task.completed` as completion of both intents. The busy branch now returns a structured `ship_busy` result (mirrors the server-side `ship_busy` 409 shape) with `current_task_id`, `current_task_type`, `current_task_description`, and a `suggested_action` hint. The voice agent prompt and the `start_task` tool schema are updated to teach the LLM to either call `steer_task` silently for refinements, tell the commander to wait for `task.completed` for separate actions, or ask when ambiguous. `_handle_start_task_tool` now surfaces failure results minimally (no event injection, no forced response cycle) so the default `result_callback` triggers a follow-up inference — mirrors the established `_handle_stop_task_tool` failure pattern. The success branch's dead `steered`-event handling is removed. BYOA `register_active` now carries `task_description` so the same busy helper works for in-process TaskAgents and remote BYOA agents. Tests rewritten: the prior busy-ship test asserted auto-steer; it now asserts the `ship_busy` contract and that no `BusSteerTaskMessage` fires. The wrapper failure test now asserts the new minimal-surface pattern. * make steer_task survive the closing-task race + prioritize the steer Two complementary changes that make explicit steer_task calls reliable in the situations where the previous commit's ship_busy contract sends the LLM down that path: 1. Closing-state pre-flight in `_steer_existing_task`. When the target in-process TaskAgent has already set `_task_finished` / `_finish_emitted` / `_cancelled`, the orchestrator returns `{error: "task_closing", retry_with: "start_task"}` instead of firing a steer that would race the terminal turn and silently drop. `_handle_steer_task_tool` surfaces this result without a forced response cycle so the voice LLM chains a fresh start_task in the same turn — the user hears one bot response, not a "steer sent" ack followed by silence. Personal and corp in-process ships go through the same code path; BYOA agents skip the check and rely on the existing ship_busy retry on the follow-up start_task. 2. Priority-wrap on TaskAgent steer injection. `_inject_steering` now wraps the steer text with a short `<priority>` directive before adding it to LLM context. The TaskAgent's original task description is itself a user message (system prompt is generic), so without the wrap the steer was just another peer instruction with no priority signal. With the wrap the LLM treats the steer as outranking the original. Orchestrator side drops its now-redundant `"Steering instruction: "` text prefix; the structured wrap is the canonical signal. Tests: new closing-state pre-flight test + new silent-task_closing wrapper test; existing steer-success test updated to assert the dropped prefix; existing _inject_steering test strengthened to assert the priority wrap is applied to the LLM context message. * restore auto-steer on busy ship; keep closing-state guard + priority wrap Walking back the no-auto-steer contract from the previous two commits. Convenient refinement UX wins out — the LLM should be able to issue a follow-up instruction via `start_task` without an extra round-trip — and the recovery for cross-intent steers (Atlas-class bugs) moves into the voice agent's task.completed handling: the agent reads the completion message and re-issues `start_task` for any unfulfilled intent. What's kept from the prior commits: - Closing-state pre-flight in `_steer_existing_task`. If the active task has already called `finished`, the steer would race the terminal turn and silently drop. Explicit `steer_task` calls return `task_closing` with a retry directive; auto-steer calls from `start_task`'s busy branch translate this to `ship_busy` so the voice LLM chains a fresh start_task. Personal and corp in-process ships symmetric; BYOA skips the check (no local liveness state). - TaskAgent `_inject_steering` `<priority>` wrap. The TaskAgent's original task description is a user message (system prompt is generic), so the wrap is what gives the steer override semantics. - Orchestrator's `"Steering instruction: "` text prefix stays removed — the `<priority>` wrap is the canonical signal. What changes back to main's behavior: - `_handle_start_task` busy branch calls `_steer_existing_task` again (with the closing-state translation above). - `_handle_start_task_tool` restores the `steered` branch so auto-steered results emit `task.steered` events. - `start_task` tool schema goes back to describing auto-steer with a new caveat: READ the task.completed message; re-issue for any intent that wasn't fulfilled. - voice_agent.md prompt updated to match. Cleanup: - Removed unused `_active_task_description_for` helper (no longer needed since busy branch doesn't synthesize a ship_busy payload with task description on every call). - Removed `task_description` kwarg from BYOACoordinator.register_active (added in the prior commit for the now-removed helper). Tests: - Rewrote `test_start_task_busy_ship_*` back to assert auto-steer fires (it's the original `test_start_task_busy_byoa_ship_steers_existing_bus_agent`). - New `test_start_task_busy_closing_task_returns_ship_busy_for_chained_start` covers the closing-state translation path. - Restored `test_start_task_tool_steered_result_queues_steered_event` (now exercising the live code path again). - Updated `test_start_task_tool_failure_surfaces_result_without_event_injection` to reflect the new ship_busy payload shape (no current_task_description). * inline wait+retry on closing-state race; trim voice agent prompt edits Two corrections to the previous commit: 1. `start_task` handles the closing-state race purely in code now — the LLM never sees ship_busy / task_closing for it. The public `_handle_start_task` calls a renamed `_handle_start_task_attempt`; when the attempt bubbles up `task_closing` (auto-steer detected the target's terminal turn), the wrapper waits up to `settings.TASK_STEER_CLOSING_WAIT_SECONDS` (default 5s) via a small `_wait_for_ship_release` poll, then retries the attempt cleanly. On wait timeout, returns a standard failure result (`error: "task_closing_timeout"`) which the existing wrapper surfaces to the LLM with a message. No new infrastructure — just poll `_locked_ships` until released. 2. Voice agent prompt edits are back to a minimal delta vs main — one sentence acknowledging that start_task on a busy slot auto-steers (priority-wrapped), and one sentence telling the agent to read the task.completed message and mention any unfulfilled steered intent to the commander (don't auto-re-issue; the commander can ask if they still want it). Explicit `steer_task` on a closing task still returns `task_closing` to the LLM — different semantics (commander asked to modify the running task, not start a new one), so the LLM-visible retry directive is right there. Tests: split the prior closing-state test into two — one for `_handle_start_task_attempt` bubbling task_closing internally, one for `_handle_start_task` doing the wait+retry. New timeout-path test covers the failure surface. Wrapper failure test updated to the new payload shape (`task_closing_timeout` instead of `ship_busy`). * use task.steered event xml shape for steer injection Replace the ad-hoc <priority>...</priority> wrap from the previous commit with the same <event name="..."> shape the TaskAgent already reads for task.progress, task.cancelled, etc. Body starts with "User has steered your task:" so the LLM has a clear directive to treat this as overriding its current plan. Before: <priority>Override your current plan with the instruction below.</priority> {text} After: <event name="task.steered"> User has steered your task: {text} </event> Test + CHANGELOG updated to match. * split steer injection into event header + raw user message Previous commit had the steer text duplicated inside the <event name="task.steered"> body. Instead, inject as two messages: 1. <event name="task.steered"> User has steered your task. Override and prioritize your current plan with the instruction below. </event> 2. {steer text} (as a normal user message) The event xml is the directive header that frames the next user message; the steer text stands on its own. Two related tests updated to assert add_message is called twice with the right shapes. * collapse steer injection back to a single event-wrapped user message Walking back the two-message split from the previous commit. One user message, content is the task.steered event xml with the directive line followed by the raw steer text — all inside the event tags. Matches how task.progress and task.cancelled events the agent already reads embed their summary text. <event name="task.steered"> User has steered your task. Override and prioritize your current plan with the instruction below. {steer_text} </event> * trim behavioral instructions out of start_task tool schema The "check what was done / mention unfulfilled intents to the commander" guidance belongs in the voice agent prompt, not the tool schema. Tool schemas describe what the tool does and what it returns; behavioral guidance is prompt-level. Same guidance already lives in voice_agent.md. * restored steering message * Tighten task steering prompt * Restore quiet start task failures * Wake corp ships for combat * Support BYOA combat wake hooks
1 parent c9a6bd4 commit ee01b34

16 files changed

Lines changed: 909 additions & 68 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Corp-ship combat now preempts normal task work by waking active TaskAgents into a combat goal or starting an idle ship task through the normal task path, while preserving the existing combat prompt and doctrine injection flow.
13+
- BYOA harnesses can now customize active combat wake behavior with an `@app.on_combat_wake` hook.
14+
1015
### Fixed
1116

1217
- `TaskAgent._inject_steering` now injects steer text as a single `<event name="task.steered">` user message with a short priority line followed by the raw steer text inline. Same event-xml shape as `task.progress` / `task.cancelled` the agent already reads — without it, the steer was just another peer instruction to the original task description (which is itself a plain user message). Orchestrator side drops its now-redundant `"Steering instruction: "` text prefix.

docs/byoa-example.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from loguru import logger
1414

15-
from gradientbang.runtime.byoa import ByoaApp, ByoaContext
15+
from gradientbang.runtime.byoa import ByoaApp, ByoaCombatWake, ByoaContext
1616

1717

1818
app = ByoaApp()
@@ -61,6 +61,28 @@ def on_session_end(ctx: ByoaContext) -> None:
6161
)
6262

6363

64+
@app.on_combat_wake
65+
def on_combat_wake(ctx: ByoaContext, wake: ByoaCombatWake) -> ByoaCombatWake | None:
66+
"""Optionally replace the combat wake before the task context resets."""
67+
68+
logger.info(
69+
"custom_byoa.combat_wake ship_id={} task_id={}",
70+
ctx.ship_id,
71+
ctx.task_id,
72+
)
73+
74+
# Return None to use the default combat goal. Return a replacement wake
75+
# when your agent should bias combat differently from the bundled prompt.
76+
return ByoaCombatWake(
77+
goal=(
78+
f"{wake.goal}\n\n"
79+
"Operator combat preference: preserve the ship first; flee if the "
80+
"opponent looks stronger, otherwise brace or attack conservatively."
81+
),
82+
context=wake.context,
83+
)
84+
85+
6486
# Optional: override model construction.
6587
#
6688
# Leave this commented to use TASK_LLM_PROVIDER, TASK_LLM_MODEL,

docs/byoa-vercel.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ Optional:
120120
- [prompt.md](../deployment/vercel/prompt.md): optional prompt starting point.
121121

122122
Most operators only edit `.env.byoa` and optionally `prompt.md`.
123+
Combat wake customization lives in the Python harness, so use a fork with
124+
[docs/byoa-example.py](byoa-example.py) when prompt-only behavior is not enough.
123125

124126
## Redeploys
125127

docs/byoa.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,27 @@ If attacked, flee unless the enemy is already badly damaged.
9191
Avoid spending fuel unless the route improves expected profit.
9292
```
9393

94+
## Combat Wake
95+
96+
When a claimed corp ship enters combat, the bot wakes the BYOA task into a combat goal before normal event broadcast. Active agents reset their task context; idle agents start through the normal task path. The bundled combat.md and ship doctrine still load from `combat.round_waiting`, so do not duplicate them in your prompt.
97+
98+
Use `@app.on_combat_wake` only when you need custom combat bias:
99+
100+
```python
101+
from gradientbang.runtime.byoa import ByoaApp, ByoaCombatWake, ByoaContext
102+
103+
app = ByoaApp()
104+
105+
@app.on_combat_wake
106+
def combat_policy(ctx: ByoaContext, wake: ByoaCombatWake) -> ByoaCombatWake | None:
107+
return ByoaCombatWake(
108+
goal=f"{wake.goal}\n\nOperator preference: flee unless clearly favored.",
109+
context=wake.context,
110+
)
111+
```
112+
113+
Return `None` to observe/log the wake without changing the default combat goal.
114+
94115
## Vercel
95116

96117
Production BYOA runs through the Vercel wake receiver in [deployment/vercel](../deployment/vercel/).

src/gradientbang/runtime/bus.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ class BusSteerTaskMessage(BusDataMessage):
123123
text: str = ""
124124

125125

126+
@dataclass
127+
class BusCombatWakeMessage(BusDataMessage):
128+
"""Urgent combat reprioritization for a running task agent.
129+
130+
The task id stays the same; the worker drops its current LLM context and
131+
treats ``goal`` as the new task instruction. Combat.md and doctrine still
132+
load from the next matching ``combat.round_waiting`` event.
133+
"""
134+
135+
task_id: str = ""
136+
goal: str = ""
137+
context: str = ""
138+
139+
126140
# ---------------------------------------------------------------------------
127141
# Typed game RPCs over the bus
128142
# ---------------------------------------------------------------------------

src/gradientbang/runtime/byoa/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,27 @@
88
99
* :class:`ByoaApp` — the default harness; instantiate and call ``.run()``
1010
for the zero-config path, or attach hooks via ``@app.prompt``, ``@app.llm``,
11-
``@app.on_session_start``, ``@app.on_session_end``.
11+
``@app.on_session_start``, ``@app.on_session_end``, ``@app.on_combat_wake``.
1212
* :class:`ByoaContext` — what hooks receive: ship_id, character_id, channel,
1313
bus_dsn, prompt, config, …
14+
* :class:`ByoaCombatWake` — replacement text returned from combat wake hooks.
1415
* :class:`ByoaAgentConfig` — runtime tunables (RPC timeouts, wake timeout,
1516
in-process corp-agent idle teardown).
1617
* :class:`ByoaConfigError` — raised for missing/malformed BYOA env vars.
1718
"""
1819

19-
from gradientbang.runtime.byoa.app import ByoaApp, ByoaConfigError, ByoaContext
20+
from gradientbang.runtime.byoa.app import (
21+
ByoaApp,
22+
ByoaCombatWake,
23+
ByoaConfigError,
24+
ByoaContext,
25+
)
2026
from gradientbang.runtime.byoa.config import ByoaAgentConfig
2127

2228
__all__ = [
2329
"ByoaAgentConfig",
2430
"ByoaApp",
31+
"ByoaCombatWake",
2532
"ByoaConfigError",
2633
"ByoaContext",
2734
]

src/gradientbang/runtime/byoa/app.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
run the bundled harness unchanged.
1818
* **Mode B** — instantiate :class:`ByoaApp` in your own ``module:main``,
1919
attach lifecycle hooks via ``@app.prompt`` / ``@app.llm`` /
20-
``@app.on_session_start`` / ``@app.on_session_end``, rebind the ``byoa``
21-
console script in your fork's ``pyproject.byoa.toml`` to point at it.
20+
``@app.on_session_start`` / ``@app.on_session_end`` /
21+
``@app.on_combat_wake``, rebind the ``byoa`` console script in your
22+
fork's ``pyproject.byoa.toml`` to point at it.
2223
"""
2324

2425
from __future__ import annotations
@@ -79,6 +80,14 @@ def from_env(cls) -> "ByoaContext":
7980
)
8081

8182

83+
@dataclass(frozen=True)
84+
class ByoaCombatWake:
85+
"""Combat wake instruction before the TaskAgent resets into combat."""
86+
87+
goal: str
88+
context: str = ""
89+
90+
8291
def _require(env_key: str) -> str:
8392
value = (os.environ.get(env_key) or "").strip()
8493
if not value:
@@ -124,6 +133,7 @@ def _load_prompt() -> Optional[str]:
124133
PromptHook = Callable[[ByoaContext], HookResult[Optional[str]]]
125134
LLMHook = Callable[[ByoaContext], HookResult[Any]]
126135
LifecycleHook = Callable[[ByoaContext], HookResult[None]]
136+
CombatWakeHook = Callable[[ByoaContext, ByoaCombatWake], HookResult[Optional[ByoaCombatWake]]]
127137

128138

129139
class ByoaApp:
@@ -153,6 +163,7 @@ def __init__(self) -> None:
153163
self._llm_hook: Optional[LLMHook] = None
154164
self._on_session_start: Optional[LifecycleHook] = None
155165
self._on_session_end: Optional[LifecycleHook] = None
166+
self._on_combat_wake: Optional[CombatWakeHook] = None
156167

157168
# ── Decorators ────────────────────────────────────────────────────
158169

@@ -187,6 +198,15 @@ def on_session_end(self, fn: LifecycleHook) -> LifecycleHook:
187198
self._on_session_end = fn
188199
return fn
189200

201+
def on_combat_wake(self, fn: CombatWakeHook) -> CombatWakeHook:
202+
"""Run before an active task is reset into combat.
203+
204+
Return ``None`` to keep the default combat wake, or return a
205+
:class:`ByoaCombatWake` with replacement goal/context text.
206+
"""
207+
self._on_combat_wake = fn
208+
return fn
209+
190210
# ── Entry points ──────────────────────────────────────────────────
191211

192212
def run(self) -> None:
@@ -235,10 +255,20 @@ async def run_async(self) -> None:
235255
bus=bus,
236256
handle_sigint=True,
237257
)
258+
259+
app = self
260+
261+
class ByoaTaskAgent(TaskAgent):
262+
"""TaskAgent with BYOA harness hooks wired in."""
263+
264+
async def _wake_for_combat(self, goal: str, context: str = "") -> None:
265+
wake = await app._apply_combat_wake(ctx, goal, context)
266+
await super()._wake_for_combat(wake.goal, wake.context)
267+
238268
# TaskAgent's character_id is the SHIP's pseudo-character (the
239269
# subject of every game tool call). Authorization is the bus channel
240270
# (capability) + corp membership + ship_byoa_configure ownership.
241-
agent = TaskAgent(
271+
agent = ByoaTaskAgent(
242272
agent_name,
243273
character_id=ctx.ship_id,
244274
is_corp_ship=True,
@@ -272,6 +302,28 @@ async def run_async(self) -> None:
272302
except Exception:
273303
logger.exception("byoa.app.bus.stop_failed")
274304

305+
async def _apply_combat_wake(
306+
self, ctx: ByoaContext, goal: str, context: str
307+
) -> ByoaCombatWake:
308+
"""Apply the optional operator combat hook; fallback is original wake."""
309+
wake = ByoaCombatWake(goal=goal, context=context)
310+
if self._on_combat_wake is None:
311+
return wake
312+
try:
313+
replacement = await _maybe_await(self._on_combat_wake, ctx, wake)
314+
except Exception:
315+
logger.exception("byoa.app.on_combat_wake.failed")
316+
return wake
317+
if replacement is None:
318+
return wake
319+
if isinstance(replacement, ByoaCombatWake):
320+
return replacement
321+
logger.warning(
322+
"byoa.app.on_combat_wake.ignored invalid_return_type={}",
323+
type(replacement).__name__,
324+
)
325+
return wake
326+
275327

276328
async def _maybe_await(fn: Callable[..., HookResult[T]], *args: Any) -> T:
277329
"""Invoke a hook that may be sync or async; await the latter."""
@@ -306,6 +358,8 @@ def _hooks_summary(app: "ByoaApp") -> str:
306358
names.append("on_session_start")
307359
if app._on_session_end is not None:
308360
names.append("on_session_end")
361+
if app._on_combat_wake is not None:
362+
names.append("on_combat_wake")
309363
return " ".join(names) if names else "(none)"
310364

311365

src/gradientbang/runtime/orchestrator.py

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from collections import deque
1515
from dataclasses import dataclass, replace
1616
from functools import wraps
17+
from types import SimpleNamespace
1718
from typing import Any, Callable, Dict, Optional, Tuple
1819

1920
from loguru import logger
@@ -65,6 +66,7 @@
6566
BusByoaPresenceMessage,
6667
BusCombatStrategyRequest,
6768
BusCombatStrategyResponse,
69+
BusCombatWakeMessage,
6870
BusCorporationQueryRequest,
6971
BusCorporationQueryResponse,
7072
BusGameEventMessage,
@@ -93,6 +95,11 @@
9395
summarize_leaderboard,
9496
summarize_ship_definitions,
9597
)
98+
from gradientbang.utils.combat import (
99+
build_combat_task_description,
100+
is_combat_participant,
101+
owned_corp_ship_participant_ids,
102+
)
96103
from gradientbang.utils.summary_formatters import list_known_ports_summary
97104
from gradientbang.utils.prompt_loader import (
98105
AVAILABLE_TOPICS,
@@ -1743,16 +1750,24 @@ async def broadcast_game_event(
17431750
self, event: Dict[str, Any], *, voice_agent_originated: bool = False
17441751
) -> None:
17451752
"""Broadcast a game event to the bus for TaskAgent children."""
1753+
event_name = event.get("event_name")
1754+
1755+
# Corp combat outranks the current task. Send the wake before the
1756+
# event broadcast; idle ships take the normal start_task path and act
1757+
# on the next live combat event.
1758+
if event_name == "combat.round_waiting":
1759+
payload = event.get("payload")
1760+
if isinstance(payload, dict):
1761+
await self._wake_corp_ship_tasks_for_combat(payload)
1762+
17461763
await self.send_bus_message(
17471764
BusGameEventMessage(
17481765
source=self.name, event=event, voice_agent_originated=voice_agent_originated
17491766
)
17501767
)
17511768

1752-
event_name = event.get("event_name")
1753-
17541769
# Cancel player ship tasks when the player enters combat.
1755-
# Corp ship tasks continue running — they're independent.
1770+
# Corp ships were handled above by waking/starting combat work.
17561771
if event_name == "combat.round_waiting":
17571772
payload = event.get("payload")
17581773
if isinstance(payload, dict) and self._is_player_combat_participant(payload):
@@ -1768,12 +1783,67 @@ async def broadcast_game_event(
17681783

17691784
def _is_player_combat_participant(self, payload: dict) -> bool:
17701785
"""Check if our character is listed in the combat participants."""
1771-
participants = payload.get("participants")
1772-
if isinstance(participants, list):
1773-
for p in participants:
1774-
if isinstance(p, dict) and p.get("id") == self._character_id:
1775-
return True
1776-
return False
1786+
return is_combat_participant(payload, self._character_id)
1787+
1788+
async def _wake_corp_ship_tasks_for_combat(self, payload: dict) -> None:
1789+
"""Wake or start owned corp ships that are participants in round one."""
1790+
if payload.get("round") not in (1, "1"):
1791+
return
1792+
for ship_character_id in owned_corp_ship_participant_ids(
1793+
payload, self._game_client.corporation_id
1794+
):
1795+
await self._wake_or_start_corp_ship_combat_task(ship_character_id, payload)
1796+
1797+
async def _wake_or_start_corp_ship_combat_task(
1798+
self, ship_character_id: str, payload: dict
1799+
) -> None:
1800+
goal = build_combat_task_description(payload, ship_character_id)
1801+
combat_id = payload.get("combat_id") or payload.get("encounter_id") or "unknown"
1802+
context = (
1803+
"Combat response task. Existing combat prompt and ship doctrine will be "
1804+
"loaded when a combat.round_waiting event is processed.\n"
1805+
f"combat_id: {combat_id}"
1806+
)
1807+
1808+
if ship_character_id in self._locked_ships:
1809+
target = self._find_steer_target_by_ship(ship_character_id)
1810+
if target is None:
1811+
logger.debug(
1812+
"combat_wake.locked_without_target ship={}",
1813+
ship_character_id[:8],
1814+
)
1815+
return
1816+
await self.send_bus_message(
1817+
BusCombatWakeMessage(
1818+
source=self.name,
1819+
target=target.agent_name,
1820+
task_id=target.framework_task_id,
1821+
goal=goal,
1822+
context=context,
1823+
)
1824+
)
1825+
await self._task_output_handler(
1826+
goal,
1827+
message_type="COMBAT",
1828+
task_id=target.framework_task_id,
1829+
task_type=target.task_type,
1830+
)
1831+
return
1832+
1833+
params = SimpleNamespace(
1834+
arguments={
1835+
"task_description": goal,
1836+
"context": context,
1837+
"ship_id": ship_character_id,
1838+
}
1839+
)
1840+
result = await self._handle_start_task(params) # type: ignore[arg-type]
1841+
if not result.get("success"):
1842+
logger.warning(
1843+
"combat_wake.start_failed ship={} error={}",
1844+
ship_character_id[:8],
1845+
result.get("error") or result,
1846+
)
17771847

17781848
async def _cancel_player_tasks_for_combat(self) -> None:
17791849
"""Cancel all active player ship tasks (not corp ship tasks)."""

0 commit comments

Comments
 (0)