Skip to content

Commit 1f4bb36

Browse files
committed
Fix title generation issue.
1 parent 53975e8 commit 1f4bb36

5 files changed

Lines changed: 97 additions & 7 deletions

File tree

python_agent_harness/agent.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ def _inject_pending_prompts(self) -> None:
130130
if self.session.plan_mode.is_plan and not self.harness_injected:
131131
self.messages.insert(
132132
len(self.messages),
133-
Message(role="user", content=self.session.plan_mode.plan_reminder()),
133+
Message(
134+
role="user",
135+
content=self.session.plan_mode.plan_reminder(),
136+
injected=True,
137+
),
134138
)
135139
self.harness_injected = True
136140
return
@@ -147,7 +151,9 @@ def _inject_pending_prompts(self) -> None:
147151
if last.role == "user" and isinstance(last.content, str) and not last.tool_call_id:
148152
insert_at = len(self.messages) - 1
149153
for i, text in enumerate(prompts):
150-
self.messages.insert(insert_at + i, Message(role="user", content=text))
154+
self.messages.insert(
155+
insert_at + i, Message(role="user", content=text, injected=True)
156+
)
151157

152158
# ------------------------------------------------------------------
153159
# compaction
@@ -362,6 +368,11 @@ def run(self) -> str | None:
362368
# file — the next turn's save would overwrite it
363369
# without ever containing it.
364370
session.auto_save(salvaged, self.system)
371+
# Elisp parity: the title is generated on the first
372+
# save, not only on clean completion — an
373+
# interrupted session still gets a meaningful name
374+
# (one-shot; no-op when already titled/pending).
375+
session.generate_session_title()
365376
else:
366377
# Loop finished: give the session a meaningful title
367378
# from the first real user message (one-shot; no-op
@@ -449,7 +460,7 @@ def safe_delta(text: str) -> None:
449460
pending=bool(self.pending),
450461
):
451462
self.messages.append(
452-
Message(role="user", content=config.NUDGE_MESSAGE)
463+
Message(role="user", content=config.NUDGE_MESSAGE, injected=True)
453464
)
454465
continue
455466
if self.error:

python_agent_harness/agent_session.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,14 @@ def _mode_prompts(self) -> dict[str, str]:
387387
def remember_user_text(self, messages: list) -> None:
388388
"""Remember the last real user message for session-title generation.
389389
390-
Skips harness-injected nudges so a title is never generated from
391-
"Review the original user request and the Task Completion Rules…".
390+
Skips harness-injected messages (nudges, plan/build-switch
391+
reminders, queued mode prompts — flagged ``injected``) so a
392+
title is never generated from "Review the original user request
393+
and the Task Completion Rules…" or a mode-switch reminder.
392394
"""
393395
nudge = config.NUDGE_MESSAGE
394396
for m in reversed(messages):
395-
if m.role == "user" and m.text() != nudge:
397+
if m.role == "user" and not m.injected and m.text() != nudge:
396398
self.store.remember_first_user_message(m.text())
397399
break
398400

@@ -411,6 +413,13 @@ def generate_session_title(self) -> None:
411413
Mirrors gptel-agent-harness--generate-session-title: one-shot per
412414
session (guarded by store.title / title_pending); on success the
413415
session file is renamed to <title>_<TS>.md.
416+
417+
Reasoning models answer with a reasoning preamble; the client
418+
merges it ahead of the real answer, so it is stripped here or
419+
the first 50 chars of the reasoning would become the session
420+
name. The session temperature is passed so the title request
421+
matches the buffer settings (elisp parity) instead of the API
422+
default.
414423
"""
415424
store = self.store
416425
if store.title or store.title_pending:
@@ -425,9 +434,18 @@ def generate_session_title(self) -> None:
425434

426435
system = read_prompt_file("title.txt")
427436
resp, _ = self.client.chat_sync(
428-
[Msg(role="user", content=first)], system=system
437+
[Msg(role="user", content=first)],
438+
system=system,
439+
temperature=self.temperature,
429440
)
430441
title = resp.text()
442+
if resp.reasoning:
443+
r = resp.reasoning
444+
if title.startswith(r):
445+
title = title[len(r):]
446+
else:
447+
title = title.replace(r, "")
448+
title = title.strip()
431449
if title:
432450
store.apply_title(title)
433451
if self.store.title:

python_agent_harness/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Message:
4141
tool_call_id: str | None = None
4242
reasoning: str | None = None
4343
name: str | None = None
44+
injected: bool = False # harness-injected (nudge/plan/build-switch), not user input
4445

4546
def to_api(self) -> dict[str, Any]:
4647
d: dict[str, Any] = {"role": self.role}

tests/test_agent.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,38 @@ def tracking_chat_sync(messages, system=None, temperature=None,
994994
AgentLoop(session, messages=[Message(role="user", content="hi there")]).run()
995995
self.assertEqual(len(session.client.chat_sync_calls), 1)
996996

997+
def test_title_strips_reasoning_and_uses_session_temperature(self):
998+
"""Reasoning content merged by the client must not leak into the
999+
title (it would become the first 50 chars of the session name),
1000+
and the title request must use the session temperature."""
1001+
session = RecordingSession()
1002+
session.tools_enabled = False
1003+
session.client.script = ["bye"]
1004+
session.client.chat_sync_calls = []
1005+
1006+
def chat_sync(messages, system=None, temperature=None,
1007+
max_tokens=None, reasoning_effort=None):
1008+
session.client.chat_sync_calls.append(temperature)
1009+
return (
1010+
Message(
1011+
role="assistant",
1012+
content=("We need to generate a title for the conversation. "
1013+
"Adding MCP support to agent harness"),
1014+
reasoning="We need to generate a title for the conversation.",
1015+
),
1016+
Usage(),
1017+
)
1018+
1019+
session.client.chat_sync = chat_sync
1020+
with mock.patch(
1021+
"python_agent_harness.prompts.read_prompt_file",
1022+
return_value="TITLE-PROMPT",
1023+
):
1024+
loop = AgentLoop(session, messages=[Message(role="user", content="add mcp")])
1025+
loop.run()
1026+
self.assertEqual(session.store.title, "Adding-MCP-support-to-agent-harness")
1027+
self.assertEqual(session.client.chat_sync_calls, [session.temperature])
1028+
9971029
def test_no_title_for_empty_first_message(self):
9981030
session = RecordingSession()
9991031
session.tools_enabled = False

tests/test_subagent_isolation.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,34 @@ def test_nudge_not_used_as_title_source(self):
134134
])
135135
self.assertEqual(s.store.first_user_message(), "real request")
136136

137+
def test_injected_prompts_not_used_as_title_source(self):
138+
"""Harness-injected messages (nudge, plan/build-switch reminders,
139+
queued mode prompts) must never become the first-user message
140+
used for session-title generation."""
141+
s = make_session(RecClient([]))
142+
s.remember_user_text([
143+
Message(role="user", content="real request"),
144+
Message(role="assistant", content="ok"),
145+
Message(
146+
role="user",
147+
content="<system-reminder>\nYour operational mode has changed "
148+
"from plan to build.\n</system-reminder>",
149+
injected=True,
150+
),
151+
])
152+
self.assertEqual(s.store.first_user_message(), "real request")
153+
154+
def test_only_injected_messages_yield_no_title_source(self):
155+
"""When every user message is harness-injected there is no real
156+
first-user message to title from."""
157+
s = make_session(RecClient([]))
158+
s.remember_user_text([
159+
Message(role="user", content="plan reminder", injected=True),
160+
Message(role="assistant", content="ok"),
161+
Message(role="user", content="nudge", injected=True),
162+
])
163+
self.assertIsNone(s.store.first_user_message())
164+
137165
def test_subagent_does_not_touch_shared_context_accounting(self):
138166
"""The sub-agent's rounds must not update the shared context
139167
ratio or calibration factor: its payload (fresh context) is

0 commit comments

Comments
 (0)