Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e8bd009
feat: add open-world evidence foundation (#407)
guangyu-reflexio Aug 3, 2026
ae87615
fix: make receipt finalization winner-aware (#408)
guangyu-reflexio Aug 3, 2026
86968da
fix: preserve offline tuner exposure evidence (#409)
guangyu-reflexio Aug 3, 2026
d5ace4c
fix: fingerprint internal user playbook metadata
guangyu-reflexio Aug 4, 2026
3e27af4
fix: retry receipt-backed learning billing
guangyu-reflexio Aug 4, 2026
e65cfe0
fix: keep receipt billing obligations retryable
guangyu-reflexio Aug 4, 2026
747a368
fix: pin receipt billing event time
guangyu-reflexio Aug 4, 2026
c8ac385
fix: use loaded run time for receipt billing
guangyu-reflexio Aug 4, 2026
713e3cd
fix: require claims for governance rebuild calls
guangyu-reflexio Aug 4, 2026
91137f0
fix: fence sqlite governance rebuild mutations
guangyu-reflexio Aug 4, 2026
411f079
fix: preserve receipt billing delivery outcomes
guangyu-reflexio Aug 4, 2026
f469f1c
fix: add always-evaluated global scheduler hooks
guangyu-reflexio Aug 4, 2026
bd788b2
fix: bound search exposures and release sqlite locks
guangyu-reflexio Aug 4, 2026
bf9fbc1
fix: resolve shared rebuild integration
guangyu-reflexio Aug 5, 2026
04bff98
fix: run always-global sweeps before bootstrap
guangyu-reflexio Aug 5, 2026
de0e60d
fix: bound session outcome migration queries
guangyu-reflexio Aug 5, 2026
87de8a4
fix: restore OSS E2E billing exemption
guangyu-reflexio Aug 5, 2026
a12a161
test: cover session outcome migration rollback
guangyu-reflexio Aug 5, 2026
d2a75bf
test: isolate generation billing from E2E mode
guangyu-reflexio Aug 5, 2026
fbafc13
test: isolate playbook retry from E2E mode
guangyu-reflexio Aug 5, 2026
f466e97
fix: bound session outcome migration memory
guangyu-reflexio Aug 5, 2026
43c3d35
fix: harden session outcome identity contracts
guangyu-reflexio Aug 5, 2026
50d8717
fix: restore fail-open usage lifecycle
guangyu-reflexio Aug 5, 2026
f1f7610
docs: clarify receipt billing retry ownership
guangyu-reflexio Aug 5, 2026
a7e7f7f
fix(storage): align trajectory depth boundary
guangyu-reflexio Aug 5, 2026
a3af3e1
fix(search): record direct playbook exposures
guangyu-reflexio Aug 5, 2026
69115ba
fix(search): cap direct playbook result batches
guangyu-reflexio Aug 5, 2026
882e0d4
docs: scope search exposure guarantee
guangyu-reflexio Aug 5, 2026
b42b3d0
fix(outcomes): constrain source labels
guangyu-reflexio Aug 5, 2026
5a6f09f
fix(outcomes): apply source contract to filters
guangyu-reflexio Aug 5, 2026
8895f4b
docs: define source label contract
guangyu-reflexio Aug 5, 2026
e30074e
fix: serialize purge heartbeat renewals
guangyu-reflexio Aug 6, 2026
57dee9f
test: make heartbeat renewal regression deterministic
guangyu-reflexio Aug 6, 2026
1a0a88a
test: prove heartbeat renewal overlap deterministically
guangyu-reflexio Aug 6, 2026
3329402
test: narrow heartbeat renewal exception capture
guangyu-reflexio Aug 6, 2026
d54ce63
test: cover unregistered session outcome acceptance
guangyu-reflexio Aug 6, 2026
0809471
Validate direct search identifier lengths
guangyu-reflexio Aug 6, 2026
928a36f
test: harden concurrent test assertions
guangyu-reflexio Aug 7, 2026
dff1331
feat: add open-world optimizer identity
guangyu-reflexio Aug 7, 2026
3249e5b
fix: upgrade generic evidence artifact storage
guangyu-reflexio Aug 8, 2026
53c41fb
fix: harden SQLite optimization storage invariants
guangyu-reflexio Aug 9, 2026
59d9519
test: align exposure boundary with queued metering
guangyu-reflexio Aug 11, 2026
5c15470
fix: align Phase 1 tests with current runtime
guangyu-reflexio Aug 11, 2026
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
6 changes: 3 additions & 3 deletions AI_AGENT_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ learns, what stays private to a user scope, and what transfers to other users.
| `user_id` | Scope for profiles and user playbooks | Use the human user, tenant, workspace, repo, or project whose preferences should be isolated. For example, use a project id when repo-specific rules should not leak into unrelated repos. |
| `agent_version` | Scope for shared agent playbooks | Use a stable agent name plus major behavior version, for example `my-agent-v1`. Keep it stable if learnings should transfer across users/projects. If you omit it, the SDK uses `DEFAULT_AGENT_VERSION` (`"agent-v0"`) — fine for a single agent, but set an explicit value before you run more than one. |
| `session_id` | Group turns for one conversation | Use the host session/conversation id. Generate a UUID if the host does not provide one. |
| `source` | Audit label | Use the integration name, for example `my-agent-plugin`. |
| `source` | Producer/workflow label | Use a non-sensitive machine label such as `support-agent:v2`. Where the API permits an empty value, empty means the source is absent. Every non-empty value must match `^[a-z0-9][a-z0-9._:-]{0,127}$` and is limited to 128 ASCII characters. Do not include user identifiers, email addresses, or other PII. |

`user_id` and `agent_version` work together:

Expand Down Expand Up @@ -234,7 +234,7 @@ Publish request fields:
| --- | --- | --- | --- |
| `user_id` | Yes | The user, tenant, workspace, repo, or project scope whose private profiles and user playbooks should be isolated. | `"alice"`, `"tenant-acme"`, `"repo-reflexio"` |
| `interactions` / `interaction_data_list` | Yes | Ordered conversation turns to publish. Include at least one turn; multi-turn correction examples are best for learning. | `[{"role": "User", "content": "Use pnpm here."}, {"role": "Assistant", "content": "Got it, I will use pnpm."}]` |
| `source` | No | Integration label for debugging and filtering. Use a stable name for the plugin, framework, or adapter. | `"my-agent-plugin"`, `"vscode-assistant"`, `"support-chatbot"` |
| `source` | No | Non-sensitive producer/workflow machine label for debugging and filtering. Empty means the source is absent where the API permits it. Every non-empty value must match `^[a-z0-9][a-z0-9._:-]{0,127}$` and is limited to 128 ASCII characters. Do not include user identifiers, email addresses, or other PII. | `"support-agent:v2"` |
| `agent_version` | Strongly recommended | The shared-agent learning boundary. Use the same value when playbooks should transfer across users. Change it when old playbooks should not transfer. | `"support-agent-v1"`, `"coding-agent-2026-05"` |
| `session_id` | Yes | Host conversation/session id. Generate a UUID if the host has no session id, and reuse it for all turns in that conversation. | `"sess_01HX8Y..."`, `"3f02b7f8-..."` |
| `skip_aggregation` | No | `False` when user playbooks should be eligible to roll up into shared agent playbooks. `True` when you want user-level extraction only. | `false` |
Expand Down Expand Up @@ -315,7 +315,7 @@ def publish_turns(
response = client.publish_interaction(
user_id=user_id,
interactions=interactions,
source="my-agent-plugin",
source="support-agent:v2",
agent_version=agent_version,
session_id=session_id,
wait_for_response=False,
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,15 @@ Publish conversations from your agent, and Reflexio closes the self-improvement
| Tool | Description |
| --- | --- |
| [uv](https://docs.astral.sh/uv/getting-started/installation/) | Python package manager |
| Python's linked SQLite runtime | >= 3.35.0 (required for local SQLite storage; not the standalone `sqlite3` CLI) |
| [Node.js](https://nodejs.org/) >= 18 | Frontend runtime |

Check the SQLite runtime linked to Python with:

```shell
uv run python -c "import sqlite3; print(sqlite3.sqlite_version_info)"
```

<p align="center">
<img src="docs/images/demo.gif" width="800px" alt="Reflexio Demo">
</p>
Expand Down
10 changes: 5 additions & 5 deletions docs/lib/methods/requests-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const requestSessionMethods: MethodDef[] = [
displayName: "Mark Session Outcome",
group: "requests-sessions",
description:
"Record the lifetime-durable first success or failure marker for an existing session. User and source are derived from the first request.",
"Record the lifetime-durable first terminal outcome for an existing session. Idempotency requires unchanged payload, governance context, and finalized trajectory/session context; changed values conflict. User and source are derived from the first request.",
httpMethod: "POST",
endpoint: "/api/session_outcome",
requestStyle: "json_body",
Expand All @@ -17,8 +17,8 @@ export const requestSessionMethods: MethodDef[] = [
name: "outcome",
type: "enum",
required: true,
description: "Terminal outcome",
enumValues: ["success", "failure"],
description: "Terminal outcome: success, failure, or unknown",
enumValues: ["success", "failure", "unknown"],
},
{ name: "occurred_at", type: "number", required: true, description: "Unix epoch seconds when the outcome occurred" },
{ name: "label", type: "string", required: false, description: "Optional label, at most 128 characters" },
Expand All @@ -43,8 +43,8 @@ export const requestSessionMethods: MethodDef[] = [
name: "outcome",
type: "enum",
required: false,
description: "Exact outcome filter",
enumValues: ["success", "failure"],
description: "Exact outcome filter: success, failure, or unknown",
enumValues: ["success", "failure", "unknown"],
},
{ name: "label", type: "string", required: false, description: "Exact label filter" },
{ name: "start_time", type: "number", required: false, description: "Inclusive minimum event time" },
Expand Down
21 changes: 18 additions & 3 deletions docs/lib/methods/unified-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const unifiedSearchMethods: MethodDef[] = [
type: "number",
required: false,
default: 5,
description: "Maximum results per entity type",
description: "Maximum results per entity type, from 1 to 100",
},
{
name: "threshold",
Expand All @@ -50,7 +50,8 @@ export const unifiedSearchMethods: MethodDef[] = [
name: "user_id",
type: "string",
required: false,
description: "Filter by user ID (profiles, user_playbooks)",
description:
"Filter by user ID (profiles, user_playbooks), at most 255 characters",
},
{
name: "entity_types",
Expand Down Expand Up @@ -90,12 +91,26 @@ export const unifiedSearchMethods: MethodDef[] = [
"Search mode: vector (embedding similarity), fts (full-text search), or hybrid (combined with RRF)",
enumValues: ["vector", "fts", "hybrid"],
},
{
name: "request_id",
type: "string",
required: false,
description:
"Caller correlation ID for the search turn, at most 255 characters",
},
{
name: "session_id",
type: "string",
required: false,
description:
"Agent session this search serves. When set, results already returned to the same session are skipped and next-best matches backfilled; searches without it neither read nor record session dedup state",
"Agent session this search serves, at most 255 characters. When set, results already returned to the same session are skipped and next-best matches backfilled; searches without it neither read nor record session dedup state",
},
{
name: "interaction_id",
type: "number",
required: false,
description:
"Caller interaction ID for the search turn; must be a positive integer (minimum 1)",
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion docs/lib/methods/user-playbooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const userPlaybookMethods: MethodDef[] = [
type: "number",
required: false,
default: 10,
description: "Maximum number of results to return",
description: "Maximum number of results to return, from 1 to 100",
},
{
name: "threshold",
Expand Down
2 changes: 1 addition & 1 deletion reflexio/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ Apply to all three modes:
| `--wait` | Block until server-side extraction finishes (returns real counts). |
| `--session-id` | Required unless each payload includes `session_id`; group publishes into one session. |
| `--agent-version` | Tag the interaction with an agent version (used by playbook filtering). |
| `--source` | Free-form source tag (defaults to `cli`). |
| `--source` | Non-sensitive producer/workflow label matching `^[a-z0-9][a-z0-9._:-]{0,127}$`; never use user identifiers or PII (defaults to `cli`). |
| `--skip-aggregation`| Extract profiles/playbooks but skip playbook aggregation. |
| `--force-extraction`| Bypass all extraction gates (`stride_size`, cheap pre-filter, LLM `should_run`) and always run extractors. |
| `--evaluation-only` | Store the request for session-level evaluation only; requires `--session-id` and skips profile/playbook extraction. |
Expand Down
36 changes: 25 additions & 11 deletions reflexio/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,9 @@ def publish_interaction(
Args:
user_id: The user ID.
interactions: List of interaction data.
source: The source of the interaction.
source: Non-sensitive producer/workflow label. A non-empty value
must match ``^[a-z0-9][a-z0-9._:-]{0,127}$`` and must not
contain user identifiers or PII.
agent_version: The agent version.
session_id: Required non-empty session ID for grouping requests.
wait_for_response: If True, the **server** waits for
Expand Down Expand Up @@ -808,7 +810,7 @@ def search_user_playbooks(
end_time (Optional[datetime]): End time for created_at filter
status_filter (Optional[list[Optional[Status]]]): Filter by status (None for CURRENT, PENDING, ARCHIVED)
tags (Optional[list[str]]): Match playbooks having any of these tags.
top_k (Optional[int]): Maximum number of results to return (default: 10)
top_k (Optional[int]): Maximum results to return, from 1 to 100 (default: 10)
threshold (Optional[float]): Similarity threshold for vector search.
When omitted, the embedding model's default is used.
enable_reformulation (Optional[bool]): Enable LLM query reformulation (default: False)
Expand Down Expand Up @@ -1112,13 +1114,20 @@ def mark_session_outcome(
value: float | None = None,
metadata: dict[str, Any] | None = None,
) -> SetSessionOutcomeResponse:
"""Record the first terminal outcome for a published session.
"""Record the immutable first outcome for a published session.

The session must already contain at least one published request. Reflexio
derives both ``user_id`` and ``source`` from the earliest request ordered
by ``(created_at, request_id)``. Only the first outcome is recorded;
retries return ``success=True`` and ``recorded=False``. Sessions are not
required to report an outcome.
by ``(created_at, request_id)``. New canonical rows bind the outcome to
the server-owned outcome contract and canonical finalized trajectory. An
exact canonical retry must match the payload, contract, and trajectory;
otherwise it is rejected with ``reason="conflicting_finalization"``.
Rolling-upgrade rows with all four identity fields null compare the
caller payload and any available server-derived session context, but
cannot compare absent contract or trajectory digests. An accepted retry
preserves all four null identity fields and returns ``success=True`` and
``recorded=False``. Sessions may report ``success``, ``failure``, or
``unknown`` and are not required to report an outcome.
"""
request = SetSessionOutcomeRequest(
session_id=session_id,
Expand Down Expand Up @@ -2724,12 +2733,14 @@ def search(
Args:
request (Optional[UnifiedSearchRequest]): The search request object (alternative to kwargs)
query (str): Search query text
top_k (Optional[int]): Maximum results per entity type (default: 5)
top_k (Optional[int]): Maximum results per entity type, from 1 to 100
(default: 5).
threshold (Optional[float]): Similarity threshold for vector search.
When omitted, the embedding model's default is used.
agent_version (Optional[str]): Filter by agent version (agent_playbooks, user_playbooks)
playbook_name (Optional[str]): Filter by playbook name (agent_playbooks, user_playbooks)
user_id (Optional[str]): Filter by user ID (profiles, user_playbooks)
user_id (Optional[str]): Filter by user ID (profiles, user_playbooks),
at most 255 characters.
tags (Optional[list[str]]): Match entities having any requested tag.
entity_types (Optional[list[str]]): Entity types to search. Valid values:
"profiles", "user_playbooks", "agent_playbooks".
Expand All @@ -2745,9 +2756,12 @@ def search(
the configured search backend supports it (default: False).
conversation_history (Optional[list[ConversationTurn] | list[dict]]): Prior conversation turns for context-aware query reformulation. Accepts ConversationTurn objects or dicts with "role" and "content" keys.
search_mode (Optional[SearchMode | str]): Search mode to use. Accepts SearchMode enum or string value ("vector", "fts", "hybrid").
request_id (Optional[str]): Caller correlation id for the search turn.
session_id (Optional[str]): Caller session id for the search turn.
interaction_id (Optional[int]): Caller interaction id for the search turn.
request_id (Optional[str]): Caller correlation ID for the search turn,
at most 255 characters.
session_id (Optional[str]): Caller session ID for the search turn,
at most 255 characters. Also enables session-scoped result deduplication.
interaction_id (Optional[int]): Caller interaction ID for the search
turn; must be a positive integer (minimum 1).

Returns:
UnifiedSearchViewResponse: Combined search results from all entity types
Expand Down
81 changes: 41 additions & 40 deletions reflexio/lib/_session_outcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,51 +53,44 @@ def mark_session_outcome(
try:
for _attempt in range(3):
context = storage.get_session_outcome_context(request.session_id)
if context.existing:
return SetSessionOutcomeResponse(
success=True,
recorded=False,
user_id=context.user_id,
source=context.source,
message="Outcome already exists",
)
if context.user_id is None or context.first_request_at is None:
return SetSessionOutcomeResponse(
success=False,
reason=SessionOutcomeFailureReason.UNKNOWN_SESSION,
message="Session has no published requests",
)
if context.user_contract_violation:
logger.warning(
"Session outcome contract violation: multiple users for session %s",
sanitise_for_log(request.session_id),
)
if context.source_contract_violation:
logger.warning(
"Session outcome contract violation: multiple sources for session %s",
sanitise_for_log(request.session_id),
)
if request.occurred_at < context.first_request_at:
return SetSessionOutcomeResponse(
success=False,
reason=SessionOutcomeFailureReason.OCCURRED_BEFORE_SESSION,
message="Outcome occurred before the session began",
user_id=context.user_id,
source=context.source,
)
provider = get_service(SESSION_OUTCOME_ACCEPTANCE)
if provider is not None:
reason = provider(
self.org_id, request, received_at, context.user_id
)
if reason is not None:
if not context.existing:
if context.user_id is None or context.first_request_at is None:
return SetSessionOutcomeResponse(
success=False,
reason=SessionOutcomeFailureReason.UNKNOWN_SESSION,
message="Session has no published requests",
)
if context.user_contract_violation:
logger.warning(
"Session outcome contract violation: multiple users for session %s",
sanitise_for_log(request.session_id),
)
if context.source_contract_violation:
logger.warning(
"Session outcome contract violation: multiple sources for session %s",
sanitise_for_log(request.session_id),
)
if request.occurred_at < context.first_request_at:
return SetSessionOutcomeResponse(
success=False,
reason=reason,
message="Outcome was not accepted",
reason=SessionOutcomeFailureReason.OCCURRED_BEFORE_SESSION,
message="Outcome occurred before the session began",
user_id=context.user_id,
source=context.source,
)
provider = get_service(SESSION_OUTCOME_ACCEPTANCE)
if provider is not None:
reason = provider(
self.org_id, request, received_at, context.user_id
)
if reason is not None:
return SetSessionOutcomeResponse(
success=False,
reason=reason,
message="Outcome was not accepted",
user_id=context.user_id,
source=context.source,
)
result = storage.record_session_outcome(
request,
created_at=received_at,
Expand All @@ -112,6 +105,10 @@ def mark_session_outcome(
message="Outcome was not recorded",
user_id=result.user_id,
source=result.source,
outcome_id=result.outcome_id,
outcome_revision=result.outcome_revision,
outcome_contract_digest=result.outcome_contract_digest,
finalized_trajectory_digest=result.finalized_trajectory_digest,
)
return SetSessionOutcomeResponse(
success=True,
Expand All @@ -123,6 +120,10 @@ def mark_session_outcome(
if result.recorded
else "Outcome already exists"
),
outcome_id=result.outcome_id,
outcome_revision=result.outcome_revision,
outcome_contract_digest=result.outcome_contract_digest,
finalized_trajectory_digest=result.finalized_trajectory_digest,
)
except Exception:
logger.exception("Failed to record session outcome")
Expand Down
Loading