Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/integrations/session-runtime-control-plane-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,36 @@ summaries, then returns:
- `reconcile_rule`: the rule that host logs remain raw facts while LoopX stores
only compact control projection.

### Raw-Material Key Classification

The builder never reads input values to decide whether they are raw material;
it classifies input key names with a typed, word-level rule. Keys are split
into words on `_`, `-`, and camelCase and matched as exact keys, whole words,
or exact word sequences, never as substrings. Every key lands in one of three
states:

| State | Effect | Examples |
| --- | --- | --- |
| compact | allowed | keys the projection reads (`status`, `summary`, `next_action`), timestamps, pointer/count suffixes only when no raw evidence is present (`catalog_id`, `login_at`), explicit safe collisions (`trace_id`, `message_id`, `log_count`), usage metrics (`token_count`, `max_tokens`) |
| raw material | `raw_material_detected`, `agent_can_continue=false`, category recorded in `raw_material_categories`; its value is never copied | `credential` (`api_key`, `access_token`, `password`, `secret_id`, `api_key_id`), `transcript` (`message`, `raw_transcript`, `messages`, `prompt`, `body`, `transcript_id`), `log` (`log_path`, `stack_trace`), `local_path` (`file_path`), `raw_output` (`stdout_tail`, `diff`, `raw_id`) |
| unclassified | reported in `unclassified_key_names` (bounded), never blocks | `backlog`, `changelog`, `logical_clock` |

The word `token` is a credential only in auth forms (`token`, `access_token`,
`auth_token`, `api_token`, `bearer_token`, `refresh_token`, `id_token`); count
forms such as `tokens_used` are compact, but a raw-material word or phrase in
the same key takes precedence over both metric and pointer shortcuts
(`tokens_password`, `raw_tokens`, `secret_id`, and `api_key_id` are raw).
`trace_id` is an explicitly safe pointer; `trace`, `stack_trace`, and
`trace_path` are logs. `log_count`, `prompt_tokens`, and `prompt_token_count`
are explicitly safe aggregates and `conversation_id` is an explicitly safe
pointer. Transcript evidence otherwise matches the exact key `message` and the
whole words `messages`, `prompt`, `prompts`, and `conversation`: `prompt_id`,
`prompt_text`, and `conversation_ref` stay raw, `message_count` and
`message_ref` are compact pointers, and `message_text` is reported as
unclassified rather than guessed either way. `log`
matches only as a whole word, so `catalog_id`, `login_at`, and `changelog` are
not flagged.

Run:

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ can continue:
| `quota_state` | yes | `eligible`, `throttled`, `monitor_quiet_skip`, `operator_gate`, or `blocked`. |
| `boundary` | yes | Read/write scope, private-data rule, and stop condition. |

### Boundary Key States

The `boundary` block reports how input keys were classified, using a typed
word-level rule (exact keys, whole words, or exact word sequences after
splitting on `_`, `-`, and camelCase; never substrings). Values from
raw-material and unclassified keys
are never copied; values from the explicit compact field contract may be used
to build the bounded projection.

- **compact**: keys the projection reads, timestamps, usage metrics
(`*_tokens`), and pointers/counts (`*_id`, `*_ref`, `*_count`, `*_at`) only
when no raw-material word or phrase is present. Known collisions such as
`trace_id`, `message_id`, `conversation_id`, `log_count`, `prompt_tokens`,
and `prompt_token_count` are explicit safe exceptions.
- **raw material**: credentials, messages/transcripts, logs, local paths, and raw tool
output. Sets `raw_material_detected`, lists `raw_material_key_names` and
`raw_material_categories`, and turns `agent_can_continue` off.
- **unclassified**: any other key. Listed in `unclassified_key_names` (bounded)
so producers can see contract drift; it never blocks continuation.

Raw-material evidence takes precedence over a generic pointer suffix. For
example, `secret_id`, `transcript_id`, `raw_id`, and `api_key_id` are raw
material, not compact pointers.

The projection should be useful even when no session is currently attached. In
that case, `runtime_id` may be `none`, `session_id` may be `null`, and
`latest_validation` should explain which runtime fact is missing.
Expand Down Expand Up @@ -129,4 +153,3 @@ A session-runtime projection is acceptable when:
4. The projection is read-only unless a separate writeback contract is enabled.
5. Public fixtures contain no raw transcripts, credentials, private links,
local paths, or internal project names.

Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,102 @@ def test_raw_material_is_flagged_not_copied() -> None:
"provide compact summaries without raw material before projection"
), payload
assert "raw_transcript" in payload["boundary"]["raw_material_key_names"], payload
assert "credential_hint" in payload["boundary"]["raw_material_key_names"], payload
assert "local_path" in payload["boundary"]["raw_material_key_names"], payload
assert payload["boundary"]["raw_material_categories"] == [
"credential",
"local_path",
"transcript",
], payload
assert_no_raw_values(payload)


# Usage metrics and pointers whose words merely contain "token", "log", or
# "trace" are compact input, never raw material.
COMPACT_LOOKALIKE_KEYS = (
"token_count",
"tokens_used",
"max_tokens",
"input_tokens",
"output_tokens",
"trace_id",
"login_at",
"catalog_id",
"dialog_id",
)
# Keys with no matching word at all are reported, not flagged.
UNCLASSIFIED_KEYS = ("logical_clock", "backlog", "changelog", "drawer")
# Raw material the substring rule used to miss: body text, credentials, local
# paths, and raw tool output in whole-word form.
RAW_MATERIAL_KEYS = (
"messages",
"content",
"prompt",
"api_key",
"password",
"body",
"output_text",
"file_path",
"diff",
"patch",
"stdout_tail",
"stderr_tail",
"log_path",
"transcript_path",
"access_token",
"auth_token",
# Raw evidence outranks pointer-like suffixes unless the full key is an
# explicit public-safe collision such as ``trace_id``.
"secret_id",
"password_id",
"transcript_id",
"raw_id",
"stdout_id",
"api_key_id",
"access_token_ref",
"tool_result_ref",
)


def test_word_level_classification_does_not_block_compact_keys() -> None:
session = {
"session_id": "session-4",
"created_at": "2026-01-01T00:04:00Z",
"next_action": "continue compact projection",
**{key: 1 for key in COMPACT_LOOKALIKE_KEYS},
**{key: "opaque" for key in UNCLASSIFIED_KEYS},
}
payload = build_session_runtime_readonly_projection(goal_id="demo-goal", sessions=[session])
boundary = payload["boundary"]
assert boundary["raw_material_detected"] is False, boundary
assert boundary["raw_material_key_names"] == [], boundary
assert boundary["unclassified_key_names"] == sorted(UNCLASSIFIED_KEYS), boundary
assert payload["first_screen"]["agent_can_continue"] is True, payload
assert payload["work_lane_contract"]["must_attempt_work"] is True, payload
assert payload["first_screen"]["recommended_action"] == (
"continue compact projection"
), payload


def test_word_level_classification_flags_every_raw_material_key() -> None:
for key in RAW_MATERIAL_KEYS:
payload = build_session_runtime_readonly_projection(
goal_id="demo-goal",
sessions=[
{
"session_id": "session-5",
"next_action": "continue compact projection",
key: "raw-value-must-not-copy",
}
],
)
boundary = payload["boundary"]
assert boundary["raw_material_key_names"] == [key], (key, boundary)
assert boundary["unclassified_key_names"] == [], (key, boundary)
assert payload["first_screen"]["agent_can_continue"] is False, (key, payload)
assert "raw-value-must-not-copy" not in json.dumps(payload), (key, payload)


def test_status_ingests_projection_first_screen() -> None:
payload = build_session_runtime_readonly_projection(
goal_id="demo-goal",
Expand Down Expand Up @@ -268,6 +360,8 @@ def main() -> int:
test_operator_gate_first_screen()
test_agent_advancement_first_screen()
test_raw_material_is_flagged_not_copied()
test_word_level_classification_does_not_block_compact_keys()
test_word_level_classification_flags_every_raw_material_key()
test_status_ingests_projection_first_screen()
print("session-runtime-readonly-projection-smoke: ok")
return 0
Expand Down
11 changes: 8 additions & 3 deletions loopx/control_plane/runtime/session_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,14 @@ def compact_session_runtime_boundary(
):
if field in boundary:
compact[field] = bool(boundary.get(field))
raw_keys = public_safe_compact_list(boundary.get("raw_material_key_names"), limit=8)
if raw_keys:
compact["raw_material_key_names"] = raw_keys
for field in (
"raw_material_key_names",
"raw_material_categories",
"unclassified_key_names",
):
values = public_safe_compact_list(boundary.get(field), limit=8)
if values:
compact[field] = values
return compact


Expand Down
Loading