Skip to content
Closed
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
7 changes: 7 additions & 0 deletions sdks/python/src/honcho/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,11 @@ async def context(
le=100,
description="Maximum number of conclusions to include in the representation.",
),
max_messages: int | None = Field(
None,
ge=1,
description="The maximum number of actual messages to include in the context (from most recent).",
),
) -> SessionContext:
"""Get optimized context for this session asynchronously."""
await self._session._honcho._ensure_workspace_async()
Expand Down Expand Up @@ -1165,6 +1170,8 @@ async def context(
query["include_most_frequent"] = include_most_frequent
if max_conclusions is not None:
query["max_conclusions"] = max_conclusions
if max_messages is not None:
query["max_messages"] = max_messages

data = await self._session._honcho._async_http_client.get(
routes.session_context(self._session.workspace_id, self._session.id),
Expand Down
8 changes: 8 additions & 0 deletions sdks/python/src/honcho/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,11 @@ def context(
le=100,
description="Maximum number of conclusions to include in the representation.",
),
max_messages: int | None = Field(
None,
ge=1,
description="The maximum number of actual messages to include in the context (from most recent).",
),
) -> SessionContext:
"""
Get optimized context for this session within a token limit.
Expand All @@ -621,6 +626,7 @@ def context(
search_max_distance: Maximum semantic distance for search results.
include_most_frequent: Whether to include the most frequent conclusions.
max_conclusions: Maximum number of conclusions to include.
max_messages: Maximum number of messages to include in context.

Returns:
A SessionContext object containing the optimized message history and
Expand Down Expand Up @@ -667,6 +673,8 @@ def context(
query["include_most_frequent"] = include_most_frequent
if max_conclusions is not None:
query["max_conclusions"] = max_conclusions
if max_messages is not None:
query["max_messages"] = max_messages

data = self._honcho._http.get(
routes.session_context(self.workspace_id, self.id),
Expand Down
4 changes: 4 additions & 0 deletions sdks/typescript/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export class Session {
search_max_distance?: number
include_most_frequent?: boolean
max_conclusions?: number
max_messages?: number
}): Promise<SessionContextResponse> {
await this._ensureWorkspace()
return this._http.get<SessionContextResponse>(
Expand Down Expand Up @@ -775,6 +776,7 @@ export class Session {
peerPerspective?: string | Peer
limitToSession?: boolean
representationOptions?: RepresentationOptions
maxMessages?: number
}): Promise<SessionContext> {
const opts = options || {}

Expand Down Expand Up @@ -802,6 +804,7 @@ export class Session {
searchQuery,
}
: undefined,
maxMessages: opts.maxMessages,
})

const context = await this._getContext({
Expand All @@ -817,6 +820,7 @@ export class Session {
include_most_frequent:
contextParams.representationOptions?.includeMostFrequent,
max_conclusions: contextParams.representationOptions?.maxConclusions,
max_messages: contextParams.maxMessages,
})

return SessionContext.fromApiResponse(this.id, context)
Expand Down
1 change: 1 addition & 0 deletions sdks/typescript/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export interface SessionContextParams {
search_max_distance?: number
include_most_frequent?: boolean
max_conclusions?: number
max_messages?: number
}

export interface SummaryResponse {
Expand Down
1 change: 1 addition & 0 deletions sdks/typescript/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ export const ContextParamsSchema = z
.object({
summary: z.boolean().optional(),
tokens: z.int('Token limit must be an integer').optional(),
maxMessages: z.number().int().min(1, 'maxMessages must be at least 1').optional(),
peerTarget: PeerIdSchema.optional(),
peerPerspective: PeerIdSchema.optional(),
limitToSession: z.boolean().optional(),
Expand Down
13 changes: 12 additions & 1 deletion src/crud/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ async def get_messages_id_range(
start_id: int = 0,
end_id: int | None = None,
token_limit: int | None = None,
max_messages: int | None = None,
) -> list[models.Message]:
"""
Get messages from a session by primary key ID range.
Expand Down Expand Up @@ -504,10 +505,20 @@ async def get_messages_id_range(
if token_limit:
# Apply token limit logic using helper function
stmt = _apply_token_limit(base_conditions, token_limit)
stmt = stmt.order_by(models.Message.id)
else:
stmt = select(models.Message).where(*base_conditions)

if max_messages:
# We want the MOST RECENT max_messages, but returned in chronological ASC order
# So we order DESC, apply limit, and wrap in a subquery to re-order ASC.
stmt = stmt.order_by(models.Message.id.desc()).limit(max_messages)
subq = stmt.subquery()
from sqlalchemy.orm import aliased
msg_alias = aliased(models.Message, subq)
stmt = select(msg_alias).order_by(msg_alias.id.asc())
else:
stmt = stmt.order_by(models.Message.id.asc())

result = await db.execute(stmt)
return list(result.scalars().all())

Expand Down
11 changes: 9 additions & 2 deletions src/routers/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ async def _get_messages_for_context_task(
session_id: str,
start_id: int,
token_limit: int,
max_messages: int | None = None,
) -> list[schemas.Message]:
"""
Fetch messages for context.
Expand All @@ -187,6 +188,7 @@ async def _get_messages_for_context_task(
session_id,
start_id=start_id,
token_limit=token_limit,
max_messages=max_messages,
)
return [schemas.Message.model_validate(msg) for msg in messages]

Expand Down Expand Up @@ -659,6 +661,11 @@ async def get_session_context(
le=100,
description="Only used if `search_query` is provided. The maximum number of conclusions to include in the representation",
),
max_messages: int | None = Query(
None,
ge=1,
description="The maximum number of actual messages to include in the context (from most recent)",
),
):
"""
Produce a context object from the Session. The caller provides an optional token limit which the entire context must fit into.
Expand All @@ -678,7 +685,7 @@ async def get_session_context(
if not peer_target:
# No representation or card needed
summary, messages = await _get_session_context_task(
db, workspace_id, session_id, token_limit, include_summary
db, workspace_id, session_id, token_limit, include_summary, max_messages=max_messages
)
Comment on lines 687 to 689
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Runtime break: unexpected keyword argument in no-peer context path.

Line 688 passes max_messages into _get_session_context_task, but that helper does not accept this argument. This will raise TypeError whenever peer_target is not provided.

🐛 Proposed fix
 async def _get_session_context_task(
     db: AsyncSession,
     workspace_id: str,
     session_id: str,
     token_limit: int,
     include_summary: bool,
+    max_messages: int | None = None,
 ) -> tuple[schemas.Summary | None, list[schemas.Message]]:
@@
     summary, messages = await summarizer.get_session_context(
         db,
         workspace_name=workspace_id,
         session_name=session_id,
         token_limit=token_limit,
         include_summary=include_summary,
+        max_messages=max_messages,
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routers/sessions.py` around lines 687 - 689, Call to
_get_session_context_task passes an unexpected keyword max_messages causing a
TypeError in the no-peer path; either remove the max_messages argument from the
call site or update the helper _get_session_context_task to accept max_messages:
add a parameter max_messages: Optional[int]=None to _get_session_context_task
(and propagate it into its logic) so both callers work, or conditionally include
max_messages only when peer_target is provided; reference
_get_session_context_task and the call site where summary, messages = await
_get_session_context_task(...) to locate and fix the mismatch.

return schemas.SessionContext(
name=session_id,
Expand Down Expand Up @@ -728,7 +735,7 @@ async def get_session_context(

# Fetch messages with the correct start_id and budget
messages = await _get_messages_for_context_task(
db, workspace_id, session_id, messages_start_id, messages_budget
db, workspace_id, session_id, messages_start_id, messages_budget, max_messages=max_messages
)

return schemas.SessionContext(
Expand Down
2 changes: 2 additions & 0 deletions src/utils/summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ async def get_session_context(
*,
cutoff: int | None = None,
include_summary: bool = True,
max_messages: int | None = None,
) -> tuple[schemas.Summary | None, list[models.Message]]:
"""
Get session context similar to the API endpoint but for internal use.
Expand Down Expand Up @@ -856,6 +857,7 @@ async def get_session_context(
start_id=messages_start_id,
end_id=cutoff,
token_limit=messages_tokens,
max_messages=max_messages,
)

return summary, messages
Expand Down
37 changes: 37 additions & 0 deletions tests/routes/test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,43 @@ def test_get_session_context_with_tokens(
assert isinstance(data["messages"], list)


def test_get_session_context_with_max_messages(
client: TestClient, sample_data: tuple[Workspace, Peer]
):
"""Test session context with max_messages parameter on a session with >100 messages"""
test_workspace, test_peer = sample_data
session_id = str(generate_nanoid())

# Create session
client.post(
f"/v3/workspaces/{test_workspace.name}/sessions",
json={"id": session_id, "peers": {test_peer.name: {}}},
)

# Add >100 messages
messages = [
{"content": f"Test message {i}", "peer_id": test_peer.name}
for i in range(110)
]
response = client.post(
f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/messages",
json={"messages": messages},
)
assert response.status_code == 201

# Get context with max_messages=10
response = client.get(
f"/v3/workspaces/{test_workspace.name}/sessions/{session_id}/context?max_messages=10",
)
assert response.status_code == 200
data = response.json()
assert "messages" in data
assert len(data["messages"]) == 10

assert data["messages"][0]["content"] == "Test message 100"
assert data["messages"][-1]["content"] == "Test message 109"


def test_get_session_context_with_all_params(
client: TestClient, sample_data: tuple[Workspace, Peer]
):
Expand Down