Skip to content
Open
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
28 changes: 21 additions & 7 deletions docs/design/conversation-compaction/conversation-compaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ lightspeed-stack
9. Append the completed turn to the conversation items (continuous history,
same conversation_id) — Llama Stack did not auto-store it (no conversation param)
10. Release per-conversation lock
11. Return response (context_status="summarized" when 1573 lands; "full" otherwise)
11. Return response (context_status="summarized" when compacted; "full" otherwise — LCORE-1573)
```

Note: when no prior summary exists and the request is below the threshold,
Expand Down Expand Up @@ -237,17 +237,30 @@ This preserves a single continuous conversation identity. The `conversation_id`

## API response changes

Add `context_status` field to `QueryResponse` and `StreamingQueryResponse`:
The `context_status` field is added in two places (LCORE-1573):

- `QueryResponse` (`src/models/api/responses/successful/query.py`) — the
non-streaming `/v1/query` response body.
- `EndEventData` (`src/models/common/agents/stream_payloads.py`) — the SSE
`end` event payload that streaming `/v1/streaming_query` clients actually
receive on the wire, alongside the analogous `truncated` signal.

``` python
context_status: str = Field(
context_status: ContextStatus = Field(
"full",
description="Context status: 'full' (no compaction), "
"'summarized' (older turns summarized).",
description='Context status: "full" (no compaction) or '
'"summarized" (older turns replaced by a summary)',
)
```

The existing `truncated` field remains deprecated.
`StreamingQueryResponse` is a documentation-only class with an empty body
(its `openapi_response()` inlines an SSE example string); adding a field
there would change nothing on the wire, so it is intentionally skipped —
only its SSE example is updated to show `context_status` in the `end` event.

The value maps directly from `CompactionResult.compacted`
(`utils/conversation_compaction.py`): `"summarized"` when True, `"full"`
otherwise. The existing `truncated` field remains deprecated.

## Configuration

Expand Down Expand Up @@ -307,7 +320,8 @@ Add `compaction` field to the root `Configuration` class.
| `src/app/endpoints/streaming_query.py` | Compaction-aware SSE path that emits the `compaction` event before summarizing (R12) |
| `src/app/endpoints/a2a.py` | Inline compaction (no SSE event); store the turn on `response.completed` |
| `src/app/endpoints/responses.py` | Silent compaction (OpenAI-compatible); store the turn via `_append_previous_response_turn` |
| `src/models/responses.py` (now relocated) | `context_status` field — deferred to LCORE-1573 |
| `src/models/api/responses/successful/query.py` | `context_status` on `QueryResponse` (non-streaming `/v1/query`) — LCORE-1573 |
| `src/models/common/agents/stream_payloads.py` | `context_status` on `EndEventData`, the streaming SSE `end` event payload (`StreamingQueryResponse` is docs-only and intentionally skipped) — LCORE-1573 |
| `src/cache/` (all backends) | `ConversationSummary` storage — LCORE-1571 |

## How compaction is invoked
Expand Down
22 changes: 20 additions & 2 deletions docs/devel_doc/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5860,6 +5860,7 @@
"ClusterQuotaLimiter": 998911,
"UserQuotaLimiter": 998911
},
"context_status": "full",
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
"input_tokens": 123,
"output_tokens": 456,
Expand Down Expand Up @@ -6265,7 +6266,7 @@
"schema": {
"type": "string"
},
"example": "data: {\"event\": \"start\", \"data\": {\"conversation_id\": \"123e4567-e89b-12d3-a456-426614174000\", \"request_id\": \"123e4567-e89b-12d3-a456-426614174001\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 0, \"token\": \"No Violation\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 1, \"token\": \"\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 2, \"token\": \"Hello\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 3, \"token\": \"!\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 4, \"token\": \" How\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 5, \"token\": \" can\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 6, \"token\": \" I\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 7, \"token\": \" assist\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 8, \"token\": \" you\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 9, \"token\": \" today\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 10, \"token\": \"?\"}}\n\ndata: {\"event\": \"turn_complete\", \"data\": {\"token\": \"Hello! How can I assist you today?\"}}\n\ndata: {\"event\": \"end\", \"data\": {\"referenced_documents\": [], \"truncated\": null, \"input_tokens\": 11, \"output_tokens\": 19}, \"available_quotas\": {}}\n\n"
"example": "data: {\"event\": \"start\", \"data\": {\"conversation_id\": \"123e4567-e89b-12d3-a456-426614174000\", \"request_id\": \"123e4567-e89b-12d3-a456-426614174001\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 0, \"token\": \"No Violation\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 1, \"token\": \"\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 2, \"token\": \"Hello\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 3, \"token\": \"!\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 4, \"token\": \" How\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 5, \"token\": \" can\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 6, \"token\": \" I\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 7, \"token\": \" assist\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 8, \"token\": \" you\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 9, \"token\": \" today\"}}\n\ndata: {\"event\": \"token\", \"data\": {\"id\": 10, \"token\": \"?\"}}\n\ndata: {\"event\": \"turn_complete\", \"data\": {\"token\": \"Hello! How can I assist you today?\"}}\n\ndata: {\"event\": \"end\", \"data\": {\"referenced_documents\": [], \"truncated\": null, \"context_status\": \"full\", \"input_tokens\": 11, \"output_tokens\": 19}, \"available_quotas\": {}}\n\n"
}
}
},
Expand Down Expand Up @@ -13503,6 +13504,13 @@
}
]
},
"ContextStatus": {
"type": "string",
"enum": [
"full",
"summarized"
]
},
"ConversationData": {
"properties": {
"conversation_id": {
Expand Down Expand Up @@ -18884,6 +18892,15 @@
true
]
},
"context_status": {
"$ref": "#/components/schemas/ContextStatus",
"description": "Context status: \"full\" (no compaction) or \"summarized\" (older turns replaced by a summary)",
"default": "full",
"examples": [
"full",
"summarized"
]
},
"input_tokens": {
"type": "integer",
"title": "Input Tokens",
Expand Down Expand Up @@ -18942,13 +18959,14 @@
"response"
],
"title": "QueryResponse",
"description": "Model representing LLM response to a query.\n\nAttributes:\n conversation_id: The optional conversation ID (UUID).\n response: The response.\n rag_chunks: Deprecated. List of RAG chunks used to generate the response.\n This information is now available in tool_results under file_search_call type.\n referenced_documents: The URLs and titles for the documents used to generate the response.\n tool_calls: List of tool calls made during response generation.\n tool_results: List of tool results.\n truncated: Whether conversation history was truncated.\n input_tokens: Number of tokens sent to LLM.\n output_tokens: Number of tokens received from LLM.\n available_quotas: Quota available as measured by all configured quota limiters.",
"description": "Model representing LLM response to a query.\n\nAttributes:\n conversation_id: The optional conversation ID (UUID).\n response: The response.\n rag_chunks: Deprecated. List of RAG chunks used to generate the response.\n This information is now available in tool_results under file_search_call type.\n referenced_documents: The URLs and titles for the documents used to generate the response.\n tool_calls: List of tool calls made during response generation.\n tool_results: List of tool results.\n truncated: Whether conversation history was truncated.\n context_status: Whether the conversation context was sent in full\n (\"full\") or older turns were replaced by a summary (\"summarized\").\n input_tokens: Number of tokens sent to LLM.\n output_tokens: Number of tokens received from LLM.\n available_quotas: Quota available as measured by all configured quota limiters.",
"examples": [
{
"available_quotas": {
"ClusterQuotaLimiter": 998911,
"UserQuotaLimiter": 998911
},
"context_status": "full",
"conversation_id": "123e4567-e89b-12d3-a456-426614174000",
"input_tokens": 123,
"output_tokens": 456,
Expand Down
1 change: 1 addition & 0 deletions src/app/endpoints/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ async def _handle_query_with_tracing(
rag_chunks=turn_summary.rag_chunks,
referenced_documents=turn_summary.referenced_documents,
truncated=False,
context_status=compaction.context_status,
input_tokens=turn_summary.token_usage.input_tokens,
output_tokens=turn_summary.token_usage.output_tokens,
available_quotas=available_quotas,
Expand Down
4 changes: 4 additions & 0 deletions src/app/endpoints/streaming_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from models.common.responses.contexts import ResponseGeneratorContext
from models.common.responses.responses_api_params import ResponsesApiParams
from models.common.responses.types import ResponseInput
from models.common.turn_summary import ContextStatus
from models.config import Action
from utils.agents.streaming import (
generate_agent_response,
Expand Down Expand Up @@ -431,6 +432,7 @@ async def generate_response_with_compaction(
)

compacted_original_input: Optional[ResponseInput] = None
context_status: ContextStatus = "full"
try:
async for item in apply_compaction(
context.client,
Expand All @@ -447,6 +449,7 @@ async def generate_response_with_compaction(
elif isinstance(item, CompactionResult):
responses_params = item.params
compacted_original_input = item.original_input
context_status = item.context_status

generator, turn_summary = await retrieve_agent_response_generator(
responses_params=responses_params,
Expand Down Expand Up @@ -495,6 +498,7 @@ async def generate_response_with_compaction(
emit_start=False,
original_input=compacted_original_input,
root_span=root_span,
context_status=context_status,
):
yield event
finally:
Expand Down
14 changes: 13 additions & 1 deletion src/models/api/responses/successful/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from models.api.responses.constants import SUCCESSFUL_RESPONSE_DESCRIPTION
from models.api.responses.successful.bases import AbstractSuccessfulResponse
from models.common.turn_summary import (
ContextStatus,
RAGChunk,
ReferencedDocument,
ToolCallSummary,
Expand All @@ -28,6 +29,8 @@ class QueryResponse(AbstractSuccessfulResponse):
tool_calls: List of tool calls made during response generation.
tool_results: List of tool results.
truncated: Whether conversation history was truncated.
context_status: Whether the conversation context was sent in full
("full") or older turns were replaced by a summary ("summarized").
input_tokens: Number of tokens sent to LLM.
output_tokens: Number of tokens received from LLM.
available_quotas: Quota available as measured by all configured quota limiters.
Expand Down Expand Up @@ -71,6 +74,13 @@ class QueryResponse(AbstractSuccessfulResponse):
examples=[False, True],
)

context_status: ContextStatus = Field(
"full",
description='Context status: "full" (no compaction) or '
'"summarized" (older turns replaced by a summary)',
examples=["full", "summarized"],
)

input_tokens: int = Field(
0,
description="Number of tokens sent to LLM",
Expand Down Expand Up @@ -113,6 +123,7 @@ class QueryResponse(AbstractSuccessfulResponse):
},
],
"truncated": False,
"context_status": "full",
"input_tokens": 123,
"output_tokens": 456,
"available_quotas": {
Expand Down Expand Up @@ -198,7 +209,8 @@ def openapi_response(cls) -> dict[str, Any]:
'"token": "Hello! How can I assist you today?"}}\n\n'
'data: {"event": "end", "data": {'
'"referenced_documents": [], '
'"truncated": null, "input_tokens": 11, "output_tokens": 19}, '
'"truncated": null, "context_status": "full", '
'"input_tokens": 11, "output_tokens": 19}, '
'"available_quotas": {}}\n\n'
),
]
Expand Down
2 changes: 2 additions & 0 deletions src/models/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
)
from models.common.transcripts import Transcript, TranscriptMetadata
from models.common.turn_summary import (
ContextStatus,
MCPListToolsSummary,
RAGChunk,
RAGContext,
Expand All @@ -44,6 +45,7 @@
"CatalogShield",
"CatalogTool",
"CatalogToolParameter",
"ContextStatus",
"ConversationData",
"ConversationDetails",
"ConversationTurn",
Expand Down
13 changes: 12 additions & 1 deletion src/models/common/agents/stream_payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from pydantic import BaseModel, ConfigDict, Field

from models.api.responses.error import AbstractErrorResponse
from models.common import ReferencedDocument, ToolCallSummary, ToolResultSummary
from models.common import (
ContextStatus,
ReferencedDocument,
ToolCallSummary,
ToolResultSummary,
)


class StreamPayloadBase(BaseModel):
Expand Down Expand Up @@ -49,6 +54,7 @@ class EndEventData(BaseModel):

referenced_documents: list[ReferencedDocument]
truncated: Optional[bool]
context_status: ContextStatus = "full"
input_tokens: int
output_tokens: int

Expand Down Expand Up @@ -149,6 +155,7 @@ def create(
cls,
*,
referenced_documents: list[ReferencedDocument],
context_status: ContextStatus,
input_tokens: int,
output_tokens: int,
available_quotas: dict[str, int],
Expand All @@ -157,6 +164,9 @@ def create(

Args:
referenced_documents: Documents referenced during the turn.
context_status: Whether the conversation context was sent in full
("full") or older turns were replaced by a summary
("summarized").
input_tokens: Input token count for the turn.
output_tokens: Output token count for the turn.
available_quotas: Remaining quota limits by quota name.
Expand All @@ -168,6 +178,7 @@ def create(
data=EndEventData(
referenced_documents=referenced_documents,
truncated=None,
context_status=context_status,
input_tokens=input_tokens,
output_tokens=output_tokens,
),
Expand Down
9 changes: 8 additions & 1 deletion src/models/common/turn_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
Used on query and streaming paths.
"""

from typing import Any, Optional
from typing import Any, Literal, Optional

from ogx_api import OpenAIResponseOutput
from pydantic import AnyUrl, BaseModel, Field

from utils.token_counter import TokenCounter

type ContextStatus = Literal["full", "summarized"]
"""How the conversation context was assembled for a turn.

``"full"`` means the full history was used; ``"summarized"`` means older
turns were replaced by a compaction summary (LCORE-1573).
"""


class RAGChunk(BaseModel):
"""Model representing a RAG chunk used in the response."""
Expand Down
7 changes: 6 additions & 1 deletion src/utils/agents/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from models.common.responses import ResponseInput
from models.common.responses.contexts import ResponseGeneratorContext
from models.common.responses.responses_api_params import ResponsesApiParams
from models.common.turn_summary import TurnSummary
from models.common.turn_summary import ContextStatus, TurnSummary
from utils.agents.error_handler import map_agent_inference_error
from utils.agents.query import (
AgentFinishReason,
Expand Down Expand Up @@ -170,6 +170,7 @@ async def generate_agent_response( # pylint: disable=too-many-statements
emit_start: bool = True,
original_input: Optional[ResponseInput] = None,
root_span: Optional[trace.Span] = None,
context_status: ContextStatus = "full",
) -> AsyncIterator[str]:
"""Wrap an agent SSE generator with cleanup logic.

Expand All @@ -189,6 +190,9 @@ async def generate_agent_response( # pylint: disable=too-many-statements
explicit-input rewrite. Used to persist the completed turn with its
structured input (preserving attachments); ``None`` otherwise.
root_span: OpenTelemetry root span for this request.
context_status: Whether the conversation context was sent in full
("full") or older turns were replaced by a summary ("summarized").
Reported to the client in the SSE end event.

Yields:
SSE-formatted strings from the wrapped generator.
Expand Down Expand Up @@ -298,6 +302,7 @@ async def generate_agent_response( # pylint: disable=too-many-statements
)
end_payload = EndStreamPayload.create(
referenced_documents=turn_summary.referenced_documents,
context_status=context_status,
input_tokens=turn_summary.token_usage.input_tokens,
output_tokens=turn_summary.token_usage.output_tokens,
available_quotas=available_quotas,
Expand Down
6 changes: 6 additions & 0 deletions src/utils/conversation_compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from log import get_logger
from models.common.responses.responses_api_params import ResponsesApiParams
from models.common.responses.types import ResponseInput
from models.common.turn_summary import ContextStatus
from models.compaction import ConversationSummary
from models.config import CompactionConfiguration, InferenceConfiguration
from utils.compaction import (
Expand Down Expand Up @@ -181,6 +182,11 @@ class CompactionResult:
compacted: bool
original_input: Optional[ResponseInput] = None

@property
def context_status(self) -> ContextStatus:
"""The API ``context_status`` value for this result (LCORE-1573)."""
return "summarized" if self.compacted else "full"


def is_marker_item(item: Any) -> bool:
"""Return True when *item* is a compaction summary marker message."""
Expand Down
Loading
Loading