Spaces:
Runtime error
Runtime error
Commit ·
1c18fa9
1
Parent(s): c318527
feat(cp4.5): persist act-call token accounting + raw_text in TurnTrace
Browse files- proteus/agents/base.py +7 -0
- proteus/agents/vanilla.py +13 -2
- proteus/runtime/session.py +4 -0
- proteus/runtime/trace.py +8 -1
- tests/agents/test_vanilla.py +29 -0
- tests/runtime/test_trace_accounting.py +28 -0
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,
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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"
|