irregular6612 commited on
Commit
1c18fa9
·
1 Parent(s): c318527

feat(cp4.5): persist act-call token accounting + raw_text in TurnTrace

Browse files
proteus/agents/base.py CHANGED
@@ -24,11 +24,18 @@ class ActResult:
24
  reasoning: The agent's stated/extracted rationale (CoT / thinking).
25
  Empty string if none was produced.
26
  raw_text: The full unprocessed model output.
 
 
 
 
27
  """
28
 
29
  action: str
30
  reasoning: str
31
  raw_text: str
 
 
 
32
 
33
 
34
  class Agent(ABC):
 
24
  reasoning: The agent's stated/extracted rationale (CoT / thinking).
25
  Empty string if none was produced.
26
  raw_text: The full unprocessed model output.
27
+ input_tokens: Token usage for the act call — prompt/input side.
28
+ output_tokens: Token usage for the act call — completion/output side.
29
+ thinking_tokens: Reasoning-token count (provider-reported or inline
30
+ ``<think>`` whitespace-split count, whichever is available).
31
  """
32
 
33
  action: str
34
  reasoning: str
35
  raw_text: str
36
+ input_tokens: int = 0
37
+ output_tokens: int = 0
38
+ thinking_tokens: int = 0
39
 
40
 
41
  class Agent(ABC):
proteus/agents/vanilla.py CHANGED
@@ -55,7 +55,7 @@ class VanillaAgent(Agent):
55
  result = self._provider.complete(
56
  messages, temperature=self._temperature, max_tokens=self._max_tokens,
57
  )
58
- answer_text, _, thinking_text = parse_thinking_tags(result.text)
59
  # Reasoning = explicit thinking block if present, else the answer body,
60
  # else the provider's separate thinking_text field.
61
  reasoning = thinking_text or result.thinking_text or answer_text
@@ -63,7 +63,18 @@ class VanillaAgent(Agent):
63
  # a decoy "ACTION:" inside a <think> block must not win over the real
64
  # post-thinking action line.
65
  action = extract_action(answer_text, available_actions) or _DEFAULT_ACTION
66
- return ActResult(action=action, reasoning=reasoning or "", raw_text=result.text)
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  def probe(
69
  self,
 
55
  result = self._provider.complete(
56
  messages, temperature=self._temperature, max_tokens=self._max_tokens,
57
  )
58
+ answer_text, parsed_thinking_tokens, thinking_text = parse_thinking_tags(result.text)
59
  # Reasoning = explicit thinking block if present, else the answer body,
60
  # else the provider's separate thinking_text field.
61
  reasoning = thinking_text or result.thinking_text or answer_text
 
63
  # a decoy "ACTION:" inside a <think> block must not win over the real
64
  # post-thinking action line.
65
  action = extract_action(answer_text, available_actions) or _DEFAULT_ACTION
66
+ return ActResult(
67
+ action=action,
68
+ reasoning=reasoning or "",
69
+ raw_text=result.text,
70
+ input_tokens=result.input_tokens,
71
+ output_tokens=result.output_tokens,
72
+ # Prefer the provider's own thinking_tokens (e.g. native structured
73
+ # reasoning from Ollama/OpenAI); fall back to the inline-<think> parser
74
+ # count when the provider reports 0. When both are non-zero the provider
75
+ # count wins (tokenizer-exact vs. a whitespace heuristic).
76
+ thinking_tokens=result.thinking_tokens or parsed_thinking_tokens,
77
+ )
78
 
79
  def probe(
80
  self,
proteus/runtime/session.py CHANGED
@@ -118,6 +118,7 @@ class SessionRunner:
118
  probe_q=probe_q,
119
  probe_a=probe_a,
120
  reasoning=result.reasoning,
 
121
  action=result.action,
122
  motive_action=optimal,
123
  habit_action=habit,
@@ -126,6 +127,9 @@ class SessionRunner:
126
  reward=reward,
127
  focal_pos=focal_pos,
128
  predator_pos=predator_pos,
 
 
 
129
  )
130
  )
131
 
 
118
  probe_q=probe_q,
119
  probe_a=probe_a,
120
  reasoning=result.reasoning,
121
+ raw_text=result.raw_text,
122
  action=result.action,
123
  motive_action=optimal,
124
  habit_action=habit,
 
127
  reward=reward,
128
  focal_pos=focal_pos,
129
  predator_pos=predator_pos,
130
+ input_tokens=result.input_tokens,
131
+ output_tokens=result.output_tokens,
132
+ thinking_tokens=result.thinking_tokens,
133
  )
134
  )
135
 
proteus/runtime/trace.py CHANGED
@@ -22,6 +22,7 @@ class TurnTrace(BaseModel):
22
  probe_q: Probe question asked (empty if probing disabled).
23
  probe_a: Probe answer given (empty if probing disabled).
24
  reasoning: The agent's stated/extracted rationale.
 
25
  action: The action the agent committed.
26
  motive_action: The motive-congruent correct action (answer key).
27
  habit_action: The inertia/baseline action (control).
@@ -32,7 +33,10 @@ class TurnTrace(BaseModel):
32
  predator_pos: Predator ``(x, y)`` BEFORE the move. Both positions
33
  serialize to JSON arrays (e.g. ``[3, 3]``) and are coerced back
34
  to tuples on load, so raw-JSONL analysis consumers will see arrays.
35
- thinking_tokens: Approximate reasoning-token count, if available.
 
 
 
36
  """
37
 
38
  turn_idx: int
@@ -40,6 +44,7 @@ class TurnTrace(BaseModel):
40
  probe_q: str = ""
41
  probe_a: str = ""
42
  reasoning: str = ""
 
43
  action: str
44
  motive_action: str
45
  habit_action: str
@@ -49,6 +54,8 @@ class TurnTrace(BaseModel):
49
  focal_pos: tuple[int, int]
50
  predator_pos: tuple[int, int]
51
  thinking_tokens: int = 0
 
 
52
 
53
 
54
  class SessionTrace(BaseModel):
 
22
  probe_q: Probe question asked (empty if probing disabled).
23
  probe_a: Probe answer given (empty if probing disabled).
24
  reasoning: The agent's stated/extracted rationale.
25
+ raw_text: Full unprocessed act-call output from the model.
26
  action: The action the agent committed.
27
  motive_action: The motive-congruent correct action (answer key).
28
  habit_action: The inertia/baseline action (control).
 
33
  predator_pos: Predator ``(x, y)`` BEFORE the move. Both positions
34
  serialize to JSON arrays (e.g. ``[3, 3]``) and are coerced back
35
  to tuples on load, so raw-JSONL analysis consumers will see arrays.
36
+ thinking_tokens: Approximate reasoning-token count (provider-reported
37
+ or inline ``<think>`` whitespace-split count), if available.
38
+ input_tokens: Act-call token usage — prompt/input side.
39
+ output_tokens: Act-call token usage — completion/output side.
40
  """
41
 
42
  turn_idx: int
 
44
  probe_q: str = ""
45
  probe_a: str = ""
46
  reasoning: str = ""
47
+ raw_text: str = ""
48
  action: str
49
  motive_action: str
50
  habit_action: str
 
54
  focal_pos: tuple[int, int]
55
  predator_pos: tuple[int, int]
56
  thinking_tokens: int = 0
57
+ input_tokens: int = 0
58
+ output_tokens: int = 0
59
 
60
 
61
  class SessionTrace(BaseModel):
tests/agents/test_vanilla.py CHANGED
@@ -74,3 +74,32 @@ def test_act_appends_action_directive_with_available_actions():
74
  assert "grid here" in user_msg
75
  assert "ACTION:" in user_msg
76
  assert "up, down, left, right, stay" in user_msg # actions list formatted into directive
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  assert "grid here" in user_msg
75
  assert "ACTION:" in user_msg
76
  assert "up, down, left, right, stay" in user_msg # actions list formatted into directive
77
+
78
+
79
+ def test_act_captures_token_accounting_from_completion_result():
80
+ # Inline <think> count comes from the parser; output_tokens from the provider.
81
+ provider = FakeProvider(responses=["<think>go up now</think>ACTION: up"])
82
+ result = VanillaAgent(provider).act("grid", VALID, "rules")
83
+ assert result.thinking_tokens == 3 # "go up now" -> 3 words
84
+ assert result.output_tokens > 0
85
+ assert result.input_tokens == 0 # FakeProvider always reports 0; documents the fake's constant, not a propagation check
86
+
87
+
88
+ def test_act_prefers_provider_thinking_tokens_when_present():
89
+ # When the provider reports its own thinking_tokens (e.g. Ollama's structured
90
+ # message.thinking), use that over the inline-tag parser count.
91
+ from proteus.providers.base import CompletionResult, LLMProvider
92
+
93
+ class _NativeTokens(LLMProvider):
94
+ @property
95
+ def model_name(self): return "native"
96
+ def complete(self, messages, temperature=0.7, max_tokens=4096):
97
+ return CompletionResult(
98
+ text="ACTION: up", input_tokens=11, output_tokens=7,
99
+ thinking_tokens=42, thinking_text="native reasoning",
100
+ )
101
+
102
+ result = VanillaAgent(_NativeTokens()).act("grid", VALID, "rules")
103
+ assert result.thinking_tokens == 42
104
+ assert result.input_tokens == 11
105
+ assert result.output_tokens == 7
tests/runtime/test_trace_accounting.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from proteus.agents import VanillaAgent
2
+ from proteus.providers import FakeProvider
3
+ from proteus.runtime import SessionRunner, SessionTrace
4
+
5
+
6
+ def _run(response):
7
+ agent = VanillaAgent(FakeProvider(responses=[response], model_name="fake-1"))
8
+ return SessionRunner(
9
+ "predator_evade", agent, seed=42, play_turns=3, use_probe=False,
10
+ ).run()
11
+
12
+
13
+ def test_act_tokens_and_raw_text_persisted_per_turn():
14
+ # FakeProvider sets output_tokens = word count; inline <think> gives thinking_tokens.
15
+ trace = _run("<think>predator is two cells east</think>ACTION: up")
16
+ t0 = trace.turns[0]
17
+ assert t0.raw_text == "<think>predator is two cells east</think>ACTION: up"
18
+ assert t0.output_tokens > 0 # propagated from CompletionResult
19
+ assert t0.input_tokens == 0 # FakeProvider reports 0; output_tokens below is the live propagation check
20
+ assert t0.thinking_tokens == 5 # whitespace-split of the think block
21
+
22
+
23
+ def test_accounting_survives_jsonl_roundtrip():
24
+ trace = _run("<think>a b c</think>ACTION: up")
25
+ reloaded = SessionTrace.model_validate_json(trace.model_dump_json())
26
+ assert reloaded.model_dump() == trace.model_dump()
27
+ assert reloaded.turns[0].thinking_tokens == 3
28
+ assert reloaded.turns[0].raw_text == "<think>a b c</think>ACTION: up"