Skip to content

Commit 804822b

Browse files
committed
feat(opencode-agents): render configured message templates
Non-system config messages go through parse_template. History and the user turn stay raw. The resolver is not a place that writes instructions.
1 parent b225113 commit 804822b

2 files changed

Lines changed: 145 additions & 3 deletions

File tree

‎packages/opencode-agents/src/launchdarkly_ai_opencode_agents/handler.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ async def _stream_impl(
576576
config if isinstance(config, dict) else {}
577577
)
578578
system = _system_text(config, variables)
579-
prompt = _user_prompt(config, user_input, history)
579+
prompt = _user_prompt(config, user_input, history, variables)
580580
try:
581581
set_input_content_attributes(
582582
span,
@@ -723,14 +723,20 @@ def _user_prompt(
723723
config: AiConfigRep,
724724
user_input: str | None,
725725
history: list[dict[str, Any]] | None,
726+
variables: dict[str, Any] | None = None,
726727
) -> str:
728+
values = variables or {}
727729
messages: list[dict[str, Any]] = []
728730
if isinstance(config, dict):
729731
raw_messages = config.get("messages")
730732
if isinstance(raw_messages, list):
731733
for message in raw_messages:
732-
if isinstance(message, dict) and message.get("role") != "system":
733-
messages.append(message)
734+
if not isinstance(message, dict) or message.get("role") == "system":
735+
continue
736+
content = message.get("content")
737+
if isinstance(content, str):
738+
message = {**message, "content": parse_template(content, values)}
739+
messages.append(message)
734740
turns = compose_history(
735741
history=[item for item in (history or []) if isinstance(item, dict)],
736742
user_input=user_input,

‎packages/opencode-agents/tests/test_handler.py‎

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,142 @@ async def test_events_receives_the_prompt_and_the_session(self) -> None:
826826
assert script.sessions == ["ses"]
827827
assert events_out[-1]["output"] == "Hi"
828828

829+
async def test_agent_instructions_render_and_history_does_not(self) -> None:
830+
handler, script = playing(
831+
[
832+
assistant_open("m1"),
833+
text_delta("m1", "p1", "Hi"),
834+
assistant_completed("m1"),
835+
]
836+
)
837+
assert handler.provides_for == ("*", "agent")
838+
events_out = [
839+
event
840+
async for event in await handler.stream(
841+
{
842+
"instructions": "Hello {{caller_email}}",
843+
"model": {"name": "claude"},
844+
"provider": {"name": "Anthropic"},
845+
},
846+
"next {{caller_email}}",
847+
None,
848+
{"caller_email": "ada@example.com"},
849+
[{"role": "user", "content": "earlier {{caller_email}}"}],
850+
)
851+
]
852+
posted = script.messages[0]
853+
assert posted["system"] == "Hello ada@example.com"
854+
assert posted["parts"] == [
855+
{
856+
"type": "text",
857+
"text": "User: earlier {{caller_email}}\n\nUser: next {{caller_email}}",
858+
}
859+
]
860+
assert events_out[-1]["output"] == "Hi"
861+
862+
async def test_templates_render_in_instructions_and_config_messages(
863+
self, monkeypatch: pytest.MonkeyPatch
864+
) -> None:
865+
from launchdarkly_ai_opencode_agents import spans as span_module
866+
867+
class _Span:
868+
def __init__(self, name: str, context: Any = None) -> None:
869+
self.name = name
870+
self.attributes: dict[str, Any] = {}
871+
872+
def set_attribute(self, key: str, value: Any) -> None:
873+
self.attributes[key] = value
874+
875+
def add_event(
876+
self, name: str, attributes: dict[str, Any] | None = None
877+
) -> None:
878+
return None
879+
880+
def set_status(self, code: Any, description: str | None = None) -> None:
881+
return None
882+
883+
def record_exception(self, exc: BaseException) -> None:
884+
return None
885+
886+
def end(self) -> None:
887+
return None
888+
889+
class _Tracer:
890+
def __init__(self) -> None:
891+
self.spans: list[_Span] = []
892+
893+
def get_tracer(self, name: str) -> _Tracer:
894+
return self
895+
896+
def start_span(self, name: str, context: Any = None) -> _Span:
897+
span = _Span(name, context)
898+
self.spans.append(span)
899+
return span
900+
901+
def set_span_in_context(self, span: _Span) -> Any:
902+
return span
903+
904+
tracer = _Tracer()
905+
monkeypatch.setattr(span_module, "trace", tracer)
906+
handler, script = playing(
907+
[
908+
assistant_open("m1"),
909+
text_delta("m1", "p1", "Hi"),
910+
assistant_completed("m1"),
911+
],
912+
capture_content=True,
913+
)
914+
config = {
915+
"instructions": "Hello {{user.name}} {{missing}}",
916+
"messages": [
917+
{"role": "user", "content": "Assist {{caller_email}}"},
918+
{"role": "system", "content": "skip {{caller_email}}"},
919+
],
920+
"model": {"name": "claude"},
921+
"provider": {"name": "Anthropic"},
922+
}
923+
variables = {"user": {"name": "Ada"}, "caller_email": "ada@example.com"}
924+
await _drain(
925+
await handler.stream(
926+
config,
927+
"ask {{caller_email}}",
928+
None,
929+
variables,
930+
[{"role": "user", "content": "earlier {{caller_email}}"}],
931+
)
932+
)
933+
posted = script.messages[0]
934+
assert posted["system"] == "Hello Ada {{missing}}"
935+
assert posted["parts"][0]["text"] == (
936+
"User: Assist ada@example.com\n\n"
937+
"User: earlier {{caller_email}}\n\n"
938+
"User: ask {{caller_email}}"
939+
)
940+
recorded = tracer.spans[0].attributes["gen_ai.system_instructions"]
941+
assert "Hello Ada {{missing}}" in recorded
942+
assert "{{user.name}}" not in recorded
943+
944+
bare, bare_script = playing(
945+
[
946+
assistant_open("m1"),
947+
text_delta("m1", "p1", "Hi"),
948+
assistant_completed("m1"),
949+
]
950+
)
951+
await _drain(
952+
await bare.stream(
953+
{
954+
"instructions": "Hello {{caller_email}}",
955+
"model": {"name": "claude"},
956+
"provider": {"name": "Acme"},
957+
},
958+
"hi",
959+
None,
960+
None,
961+
)
962+
)
963+
assert bare_script.messages[0]["system"] == "Hello {{caller_email}}"
964+
829965
async def test_other_sessions_are_ignored_and_idle_ends_the_run(self) -> None:
830966
events = await collect(
831967
[

0 commit comments

Comments
 (0)