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
Empty file added .codex
Empty file.
30 changes: 30 additions & 0 deletions .env.local-api.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
APP_ENV=debug
API_EXPORT_PORT=8029
ROOT_API_BEARER_TOKEN=replace-with-local-dev-token

DATABASE_HOST=127.0.0.1
DATABASE_EXPORT_PORT=15432
DATABASE_USER=replace-with-db-user
DATABASE_PASSWORD=replace-with-db-password
DATABASE_NAME=replace-with-db-name

REDIS_HOST=127.0.0.1
REDIS_EXPORT_PORT=16379
REDIS_PASSWORD=replace-with-redis-password

RABBITMQ_HOST=127.0.0.1
RABBITMQ_EXPORT_PORT=15672
RABBITMQ_USER=replace-with-rabbitmq-user
RABBITMQ_PASSWORD=replace-with-rabbitmq-password
RABBITMQ_VHOST=/
RABBITMQ_VHOST_ENCODED=%2F

S3_ENDPOINT=http://127.0.0.1:19000
S3_INTERNAL_ENDPOINT=http://127.0.0.1:19000
S3_REGION=auto
S3_ACCESS_KEY=replace-with-s3-access-key
S3_SECRET_KEY=replace-with-s3-secret-key
S3_BUCKET=replace-with-s3-bucket

CORE_BASE_URL=http://127.0.0.1:8019
OTEL_EXPORTER_OTLP_ENDPOINT=
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
plans/
.claude
.cursorrules
plans/
.env.local-api
src/server/.env.local-api
.DS_Store
.agents
skills-lock.json
Expand Down
27 changes: 10 additions & 17 deletions docs/content/docs/(guides)/engineering/editing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,25 @@ Apply edit strategies when retrieving messages to manage context window size. Th

The `get_messages` response includes `this_time_tokens` - the total token count of returned messages. Use this to:
- Check current context window size
- Decide when to apply edit strategies
- Apply edit strategies only when needed
- Determine when to [reset the prompt cache](/engineering/cache)

<CodeGroup>
```python title="Python"
result = client.sessions.get_messages(session_id="session-uuid")
result = client.sessions.get_messages(
session_id="session-uuid",
edit_strategies=[{"type": "token_limit", "params": {"limit_tokens": 30000}}],
editing_trigger={"token_gte": 50000},
)
print(f"Current tokens: {result.this_time_tokens}")

if result.this_time_tokens > 50000:
# Apply strategies to reduce context
result = client.sessions.get_messages(
session_id="session-uuid",
edit_strategies=[{"type": "token_limit", "params": {"limit_tokens": 30000}}]
)
```

```typescript title="TypeScript"
let result = await client.sessions.getMessages("session-uuid");
const result = await client.sessions.getMessages("session-uuid", {
editStrategies: [{ type: "token_limit", params: { limit_tokens: 30000 } }],
editingTrigger: { token_gte: 50000 },
});
console.log(`Current tokens: ${result.thisTimeTokens}`);

if (result.thisTimeTokens > 50000) {
// Apply strategies to reduce context
result = await client.sessions.getMessages("session-uuid", {
editStrategies: [{ type: "token_limit", params: { limit_tokens: 30000 } }],
});
}
```
</CodeGroup>

Expand Down
23 changes: 22 additions & 1 deletion src/client/acontext-py/src/acontext/_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Utility functions for the acontext Python client."""

from typing import Any, Iterable
from typing import Any, Iterable, Mapping


def bool_to_str(value: bool) -> str:
Expand Down Expand Up @@ -58,3 +58,24 @@ def validate_edit_strategies(edit_strategies: Iterable[dict[str, Any]]) -> None:
raise ValueError("gt_token must be an integer >= 1")
if gt_token < 1:
raise ValueError("gt_token must be >= 1")


def validate_editing_trigger(editing_trigger: Mapping[str, Any]) -> None:
"""Validate editing trigger before sending to the API."""
if len(editing_trigger) == 0:
raise ValueError("editing_trigger must include at least one supported field")

# Keep the SDK strict so unsupported trigger names fail locally with a
# clearer error instead of making a round trip to the API first.
allowed_keys = {"token_gte"}
unknown_keys = set(editing_trigger.keys()) - allowed_keys
if unknown_keys:
unknown = ", ".join(sorted(unknown_keys))
raise ValueError(f"unsupported editing_trigger field(s): {unknown}")

if "token_gte" in editing_trigger:
token_gte = editing_trigger["token_gte"]
if isinstance(token_gte, bool) or not isinstance(token_gte, int):
raise ValueError("token_gte must be an integer > 0")
if token_gte <= 0:
raise ValueError("token_gte must be > 0")
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from dataclasses import asdict
from typing import Any, BinaryIO, Literal, Optional, List

from .._utils import build_params, validate_edit_strategies
from .._utils import build_params, validate_edit_strategies, validate_editing_trigger
from ..client_types import AsyncRequesterProtocol
from ..messages import AcontextMessage
from ..types.common import FlagResponse
from ..types.session import (
EditStrategy,
EditingTrigger,
CopySessionResult,
GetMessagesOutput,
GetTasksOutput,
Expand Down Expand Up @@ -374,6 +375,8 @@ async def get_messages(
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
time_desc: bool | None = None,
edit_strategies: Optional[List[EditStrategy]] = None,
# editing_trigger triggers edit_strategies (v0 supports {"token_gte": int}).
editing_trigger: EditingTrigger | BaseModel | None = None,
pin_editing_strategies_at_message: str | None = None,
) -> GetMessagesOutput:
"""Get messages for a session.
Expand All @@ -394,6 +397,7 @@ async def get_messages(
- Middle out: [{"type": "middle_out", "params": {"token_reduce_to": 5000}}]
- Token limit: [{"type": "token_limit", "params": {"limit_tokens": 20000}}]
Defaults to None.
editing_trigger: Trigger config for edit_strategies, e.g. {"token_gte": 30000}. Defaults to None.
pin_editing_strategies_at_message: Message ID to pin editing strategies at.
When provided, strategies are only applied to messages up to and including
this message ID, keeping subsequent messages unchanged. This helps maintain
Expand All @@ -419,6 +423,13 @@ async def get_messages(
if edit_strategies is not None:
validate_edit_strategies(edit_strategies)
params["edit_strategies"] = json.dumps(edit_strategies)
if editing_trigger is not None:
# Keep async behavior aligned with the sync client: normalize model
# inputs first, then validate and serialize the exact API payload.
if isinstance(editing_trigger, BaseModel):
editing_trigger = editing_trigger.model_dump()
validate_editing_trigger(editing_trigger)
params["editing_trigger"] = json.dumps(editing_trigger)
if pin_editing_strategies_at_message is not None:
params["pin_editing_strategies_at_message"] = (
pin_editing_strategies_at_message
Expand Down
13 changes: 12 additions & 1 deletion src/client/acontext-py/src/acontext/resources/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
from dataclasses import asdict
from typing import Any, BinaryIO, Literal, Optional, List

from .._utils import build_params, validate_edit_strategies
from .._utils import build_params, validate_edit_strategies, validate_editing_trigger
from ..client_types import RequesterProtocol
from ..messages import AcontextMessage
from ..types.common import FlagResponse
from ..types.session import (
EditStrategy,
EditingTrigger,
CopySessionResult,
GetMessagesOutput,
GetTasksOutput,
Expand Down Expand Up @@ -374,6 +375,8 @@ def get_messages(
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
time_desc: bool | None = None,
edit_strategies: Optional[List[EditStrategy]] = None,
# editing_trigger triggers edit_strategies (v0 supports {"token_gte": int}).
editing_trigger: EditingTrigger | BaseModel | None = None,
pin_editing_strategies_at_message: str | None = None,
) -> GetMessagesOutput:
"""Get messages for a session.
Expand All @@ -394,6 +397,7 @@ def get_messages(
- Middle out: [{"type": "middle_out", "params": {"token_reduce_to": 5000}}]
- Token limit: [{"type": "token_limit", "params": {"limit_tokens": 20000}}]
Defaults to None.
editing_trigger: Trigger config for edit_strategies, e.g. {"token_gte": 30000}. Defaults to None.
pin_editing_strategies_at_message: Message ID to pin editing strategies at.
When provided, strategies are only applied to messages up to and including
this message ID, keeping subsequent messages unchanged. This helps maintain
Expand All @@ -419,6 +423,13 @@ def get_messages(
if edit_strategies is not None:
validate_edit_strategies(edit_strategies)
params["edit_strategies"] = json.dumps(edit_strategies)
if editing_trigger is not None:
# Accept either a plain dict or a caller-provided Pydantic model so
# the SDK surface matches the existing flexibility of edit_strategies.
if isinstance(editing_trigger, BaseModel):
editing_trigger = editing_trigger.model_dump()
validate_editing_trigger(editing_trigger)
params["editing_trigger"] = json.dumps(editing_trigger)
if pin_editing_strategies_at_message is not None:
params["pin_editing_strategies_at_message"] = (
pin_editing_strategies_at_message
Expand Down
2 changes: 2 additions & 0 deletions src/client/acontext-py/src/acontext/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from .session import (
Asset,
EditingTrigger,
GetMessagesOutput,
GetTasksOutput,
ListSessionsOutput,
Expand Down Expand Up @@ -67,6 +68,7 @@
"UpdateArtifactResp",
# Session types
"Asset",
"EditingTrigger",
"GetMessagesOutput",
"GetTasksOutput",
"ListSessionsOutput",
Expand Down
11 changes: 11 additions & 0 deletions src/client/acontext-py/src/acontext/types/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ class MiddleOutStrategy(TypedDict):
]


class EditingTrigger(TypedDict, total=False):
"""Trigger config for applying edit strategies.

Attributes:
token_gte: Apply edit strategies only when the current token count is
greater than or equal to this value.
"""

token_gte: NotRequired[int]


class Asset(BaseModel):
"""Asset model representing a file asset."""

Expand Down
10 changes: 10 additions & 0 deletions src/client/acontext-ts/src/resources/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { buildParams, validateUUID } from '../utils';
import {
EditStrategy,
EditStrategySchema,
EditingTrigger,
EditingTriggerSchema,
CopySessionResult,
CopySessionResultSchema,
FlagResponse,
Expand Down Expand Up @@ -340,6 +342,7 @@ export class SessionsAPI {
* @param options.format - The format of the messages ('acontext', 'openai', 'anthropic', or 'gemini').
* @param options.timeDesc - Order by created_at descending if true, ascending if false.
* @param options.editStrategies - Optional list of edit strategies to apply before format conversion.
* @param options.editingTrigger - Optional trigger config for editStrategies (v0 supports { token_gte: number }).
* Examples:
* - Remove tool results: [{ type: 'remove_tool_result', params: { keep_recent_n_tool_results: 3 } }]
* - Remove large tool results: [{ type: 'remove_tool_result', params: { gt_token: 100 } }]
Expand All @@ -364,6 +367,7 @@ export class SessionsAPI {
format?: 'acontext' | 'openai' | 'anthropic' | 'gemini';
timeDesc?: boolean | null;
editStrategies?: Array<EditStrategy> | null;
editingTrigger?: EditingTrigger | null;
pinEditingStrategiesAtMessage?: string | null;
}
): Promise<GetMessagesOutput> {
Expand All @@ -387,6 +391,12 @@ export class SessionsAPI {
EditStrategySchema.array().parse(options.editStrategies);
params.edit_strategies = JSON.stringify(options.editStrategies);
}
if (options?.editingTrigger !== undefined && options?.editingTrigger !== null) {
// Validate before serializing so unsupported trigger shapes fail at the
// SDK boundary rather than after an API request.
EditingTriggerSchema.parse(options.editingTrigger);
params.editing_trigger = JSON.stringify(options.editingTrigger);
}
if (options?.pinEditingStrategiesAtMessage !== undefined && options?.pinEditingStrategiesAtMessage !== null) {
params.pin_editing_strategies_at_message = options.pinEditingStrategiesAtMessage;
}
Expand Down
14 changes: 14 additions & 0 deletions src/client/acontext-ts/src/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,17 @@ export const EditStrategySchema = z.union([
]);

export type EditStrategy = z.infer<typeof EditStrategySchema>;

/**
* Trigger config for applying edit strategies.
* v0 supports only token_gte.
*/
export const EditingTriggerSchema = z.object({
token_gte: z.number().int().positive().optional(),
}).strict().refine((value) => Object.keys(value).length > 0, {
// Mirror the API's "at least one supported trigger" rule so empty objects
// are rejected consistently across clients and server.
message: 'editingTrigger must include at least one supported field',
});

export type EditingTrigger = z.infer<typeof EditingTriggerSchema>;
30 changes: 30 additions & 0 deletions src/server/.env.local-api.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
APP_ENV=debug
API_EXPORT_PORT=8029
ROOT_API_BEARER_TOKEN=replace-with-local-dev-token

DATABASE_HOST=127.0.0.1
DATABASE_EXPORT_PORT=15432
DATABASE_USER=replace-with-db-user
DATABASE_PASSWORD=replace-with-db-password
DATABASE_NAME=replace-with-db-name

REDIS_HOST=127.0.0.1
REDIS_EXPORT_PORT=16379
REDIS_PASSWORD=replace-with-redis-password

RABBITMQ_HOST=127.0.0.1
RABBITMQ_EXPORT_PORT=15672
RABBITMQ_USER=replace-with-rabbitmq-user
RABBITMQ_PASSWORD=replace-with-rabbitmq-password
RABBITMQ_VHOST=/
RABBITMQ_VHOST_ENCODED=%2F

S3_ENDPOINT=http://127.0.0.1:19000
S3_INTERNAL_ENDPOINT=http://127.0.0.1:19000
S3_REGION=auto
S3_ACCESS_KEY=replace-with-s3-access-key
S3_SECRET_KEY=replace-with-s3-secret-key
S3_BUCKET=replace-with-s3-bucket

CORE_BASE_URL=http://127.0.0.1:8019
OTEL_EXPORTER_OTLP_ENDPOINT=
Loading
Loading