Skip to content

Commit 185b2cc

Browse files
committed
feat(api): add editing_trigger to gate edit_strategies
1 parent dae24b7 commit 185b2cc

File tree

6 files changed

+146
-30
lines changed

6 files changed

+146
-30
lines changed

docs/engineering/editing.mdx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,25 @@ Apply edit strategies when retrieving messages to manage context window size. Th
99

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

1515
<CodeGroup>
1616
```python Python
17-
result = client.sessions.get_messages(session_id="session-uuid")
17+
result = client.sessions.get_messages(
18+
session_id="session-uuid",
19+
edit_strategies=[{"type": "token_limit", "params": {"limit_tokens": 30000}}],
20+
editing_trigger={"token_gte": 50000},
21+
)
1822
print(f"Current tokens: {result.this_time_tokens}")
19-
20-
if result.this_time_tokens > 50000:
21-
# Apply strategies to reduce context
22-
result = client.sessions.get_messages(
23-
session_id="session-uuid",
24-
edit_strategies=[{"type": "token_limit", "params": {"limit_tokens": 30000}}]
25-
)
2623
```
2724

2825
```typescript TypeScript
29-
let result = await client.sessions.getMessages("session-uuid");
26+
const result = await client.sessions.getMessages("session-uuid", {
27+
editStrategies: [{ type: "token_limit", params: { limit_tokens: 30000 } }],
28+
editingTrigger: { token_gte: 50000 },
29+
});
3030
console.log(`Current tokens: ${result.thisTimeTokens}`);
31-
32-
if (result.thisTimeTokens > 50000) {
33-
// Apply strategies to reduce context
34-
result = await client.sessions.getMessages("session-uuid", {
35-
editStrategies: [{ type: "token_limit", params: { limit_tokens: 30000 } }],
36-
});
37-
}
3831
```
3932
</CodeGroup>
4033

src/client/acontext-py/src/acontext/resources/async_sessions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ async def get_messages(
296296
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
297297
time_desc: bool | None = None,
298298
edit_strategies: Optional[List[EditStrategy]] = None,
299+
# editing_trigger triggers edit_strategies (v0 supports {"token_gte": int}).
300+
editing_trigger: dict[str, Any] | None = None,
299301
pin_editing_strategies_at_message: str | None = None,
300302
# auto_trim_token_threshold is the token threshold for auto-trim.
301303
auto_trim_token_threshold: int | None = None,
@@ -318,6 +320,7 @@ async def get_messages(
318320
- Middle out: [{"type": "middle_out", "params": {"token_reduce_to": 5000}}]
319321
- Token limit: [{"type": "token_limit", "params": {"limit_tokens": 20000}}]
320322
Defaults to None.
323+
editing_trigger: Trigger config for edit_strategies, e.g. {"token_gte": 30000}. Defaults to None.
321324
pin_editing_strategies_at_message: Message ID to pin editing strategies at.
322325
When provided, strategies are only applied to messages up to and including
323326
this message ID, keeping subsequent messages unchanged. This helps maintain
@@ -343,6 +346,10 @@ async def get_messages(
343346
)
344347
if edit_strategies is not None:
345348
params["edit_strategies"] = json.dumps(edit_strategies)
349+
if editing_trigger is not None:
350+
if isinstance(editing_trigger, BaseModel):
351+
editing_trigger = editing_trigger.model_dump()
352+
params["editing_trigger"] = json.dumps(editing_trigger)
346353
if pin_editing_strategies_at_message is not None:
347354
params["pin_editing_strategies_at_message"] = (
348355
pin_editing_strategies_at_message

src/client/acontext-py/src/acontext/resources/sessions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ def get_messages(
296296
format: Literal["acontext", "openai", "anthropic", "gemini"] = "openai",
297297
time_desc: bool | None = None,
298298
edit_strategies: Optional[List[EditStrategy]] = None,
299+
# editing_trigger triggers edit_strategies (v0 supports {"token_gte": int}).
300+
editing_trigger: dict[str, Any] | None = None,
299301
pin_editing_strategies_at_message: str | None = None,
300302
# auto_trim_token_threshold is the token threshold for auto-trim.
301303
auto_trim_token_threshold: int | None = None,
@@ -318,6 +320,7 @@ def get_messages(
318320
- Middle out: [{"type": "middle_out", "params": {"token_reduce_to": 5000}}]
319321
- Token limit: [{"type": "token_limit", "params": {"limit_tokens": 20000}}]
320322
Defaults to None.
323+
editing_trigger: Trigger config for edit_strategies, e.g. {"token_gte": 30000}. Defaults to None.
321324
pin_editing_strategies_at_message: Message ID to pin editing strategies at.
322325
When provided, strategies are only applied to messages up to and including
323326
this message ID, keeping subsequent messages unchanged. This helps maintain
@@ -343,6 +346,10 @@ def get_messages(
343346
)
344347
if edit_strategies is not None:
345348
params["edit_strategies"] = json.dumps(edit_strategies)
349+
if editing_trigger is not None:
350+
if isinstance(editing_trigger, BaseModel):
351+
editing_trigger = editing_trigger.model_dump()
352+
params["editing_trigger"] = json.dumps(editing_trigger)
346353
if pin_editing_strategies_at_message is not None:
347354
params["pin_editing_strategies_at_message"] = (
348355
pin_editing_strategies_at_message

src/client/acontext-ts/src/resources/sessions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export class SessionsAPI {
230230
* @param options.format - The format of the messages ('acontext', 'openai', 'anthropic', or 'gemini').
231231
* @param options.timeDesc - Order by created_at descending if true, ascending if false.
232232
* @param options.editStrategies - Optional list of edit strategies to apply before format conversion.
233+
* @param options.editingTrigger - Optional trigger config for editStrategies (v0 supports { token_gte: number }).
233234
* Examples:
234235
* - Remove tool results: [{ type: 'remove_tool_result', params: { keep_recent_n_tool_results: 3 } }]
235236
* - Middle out: [{ type: 'middle_out', params: { token_reduce_to: 5000 } }]
@@ -250,6 +251,7 @@ export class SessionsAPI {
250251
format?: 'acontext' | 'openai' | 'anthropic' | 'gemini';
251252
timeDesc?: boolean | null;
252253
editStrategies?: Array<EditStrategy> | null;
254+
editingTrigger?: Record<string, unknown> | null;
253255
pinEditingStrategiesAtMessage?: string | null;
254256
}
255257
): Promise<GetMessagesOutput> {
@@ -269,6 +271,9 @@ export class SessionsAPI {
269271
if (options?.editStrategies !== undefined && options?.editStrategies !== null) {
270272
params.edit_strategies = JSON.stringify(options.editStrategies);
271273
}
274+
if (options?.editingTrigger !== undefined && options?.editingTrigger !== null) {
275+
params.editing_trigger = JSON.stringify(options.editingTrigger);
276+
}
272277
if (options?.pinEditingStrategiesAtMessage !== undefined && options?.pinEditingStrategiesAtMessage !== null) {
273278
params.pin_editing_strategies_at_message = options.pinEditingStrategiesAtMessage;
274279
}

src/server/api/go/internal/modules/handler/session.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ type GetMessagesReq struct {
470470
Format string `form:"format,default=openai" json:"format" binding:"omitempty,oneof=acontext openai anthropic gemini" example:"openai" enums:"acontext,openai,anthropic,gemini"`
471471
TimeDesc bool `form:"time_desc,default=false" json:"time_desc" example:"false"`
472472
EditStrategies string `form:"edit_strategies" json:"edit_strategies" example:"[{\"type\":\"remove_tool_result\",\"params\":{\"keep_recent_n_tool_results\":3}}]"`
473+
EditingTrigger string `form:"editing_trigger" json:"editing_trigger" example:"{\"token_gte\":30000}"`
473474
PinEditingStrategiesAtMessage string `form:"pin_editing_strategies_at_message" json:"pin_editing_strategies_at_message" example:""`
474475
// AutoTrim holds optional auto-trim parameters.
475476
AutoTrim *AutoTrim `form:"auto_trim" json:"auto_trim"`
@@ -489,6 +490,7 @@ type GetMessagesReq struct {
489490
// @Param format query string false "Format to convert messages to: acontext (original), openai (default), anthropic, gemini." enums(acontext,openai,anthropic,gemini)
490491
// @Param time_desc query string false "Order by created_at descending if true, ascending if false (default false)" example(false)
491492
// @Param edit_strategies query string false "JSON array of edit strategies to apply before format conversion" example([{"type":"remove_tool_result","params":{"keep_recent_n_tool_results":3}}])
493+
// @Param editing_trigger query string false "JSON object trigger for edit_strategies. v0 supports only {\"token_gte\": <int>} (OR semantics when more triggers are added)." example({"token_gte":30000})
492494
// @Param pin_editing_strategies_at_message query string false "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 prompt cache stability by preserving a stable prefix. The response will include edit_at_message_id indicating where strategies were applied." example()
493495
// @Security BearerAuth
494496
// @Success 200 {object} serializer.Response{data=service.GetMessagesOutput}
@@ -544,6 +546,58 @@ func (h *SessionHandler) GetMessages(c *gin.Context) {
544546
}
545547
}
546548

549+
// Parse editing_trigger if provided (v0 supports only token_gte).
550+
var editingTrigger *service.EditingTrigger
551+
if req.EditingTrigger != "" {
552+
if req.EditStrategies == "" {
553+
c.JSON(http.StatusBadRequest, serializer.ParamErr("editing_trigger requires edit_strategies", errors.New("missing edit_strategies")))
554+
return
555+
}
556+
557+
var raw map[string]interface{}
558+
if err := sonic.Unmarshal([]byte(req.EditingTrigger), &raw); err != nil {
559+
c.JSON(http.StatusBadRequest, serializer.ParamErr("invalid editing_trigger JSON", err))
560+
return
561+
}
562+
allowedTriggerKeys := map[string]struct{}{
563+
"token_gte": {},
564+
}
565+
for k := range raw {
566+
if _, ok := allowedTriggerKeys[k]; !ok {
567+
c.JSON(http.StatusBadRequest, serializer.ParamErr("invalid editing_trigger", fmt.Errorf("unsupported trigger: %s", k)))
568+
return
569+
}
570+
}
571+
572+
var trig service.EditingTrigger
573+
if err := sonic.Unmarshal([]byte(req.EditingTrigger), &trig); err != nil {
574+
c.JSON(http.StatusBadRequest, serializer.ParamErr("invalid editing_trigger JSON", err))
575+
return
576+
}
577+
578+
triggerValidators := map[string]func(service.EditingTrigger) error{
579+
"token_gte": func(t service.EditingTrigger) error {
580+
if t.TokenGte == nil || *t.TokenGte <= 0 {
581+
return errors.New("token_gte must be > 0")
582+
}
583+
return nil
584+
},
585+
}
586+
for k := range raw {
587+
validate, ok := triggerValidators[k]
588+
if !ok {
589+
// Should be unreachable due to allowedTriggerKeys check above.
590+
c.JSON(http.StatusBadRequest, serializer.ParamErr("invalid editing_trigger", fmt.Errorf("unsupported trigger: %s", k)))
591+
return
592+
}
593+
if err := validate(trig); err != nil {
594+
c.JSON(http.StatusBadRequest, serializer.ParamErr("invalid editing_trigger."+k, err))
595+
return
596+
}
597+
}
598+
editingTrigger = &trig
599+
}
600+
547601
// Map handler auto-trim to service auto-trim.
548602
var autoTrim *service.AutoTrim
549603
// Build service auto-trim only when request auto-trim exists.
@@ -560,6 +614,7 @@ func (h *SessionHandler) GetMessages(c *gin.Context) {
560614
AssetExpire: time.Hour * 24,
561615
TimeDesc: req.TimeDesc,
562616
EditStrategies: editStrategies,
617+
EditingTrigger: editingTrigger,
563618
PinEditingStrategiesAtMessage: req.PinEditingStrategiesAtMessage,
564619
// Pass auto-trim config to service.
565620
AutoTrim: autoTrim,

src/server/api/go/internal/modules/service/session.go

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/memodb-io/Acontext/internal/modules/repo"
1919
"github.com/memodb-io/Acontext/internal/pkg/editor"
2020
"github.com/memodb-io/Acontext/internal/pkg/paging"
21+
"github.com/memodb-io/Acontext/internal/pkg/tokenizer"
2122
"github.com/redis/go-redis/v9"
2223
"go.uber.org/zap"
2324
"gorm.io/datatypes"
@@ -376,18 +377,27 @@ func (s *sessionService) StoreMessage(ctx context.Context, in StoreMessageInput)
376377
}
377378

378379
type GetMessagesInput struct {
379-
SessionID uuid.UUID `json:"session_id"`
380-
Limit int `json:"limit"`
381-
Cursor string `json:"cursor"`
382-
WithAssetPublicURL bool `json:"with_public_url"`
383-
AssetExpire time.Duration `json:"asset_expire"`
384-
TimeDesc bool `json:"time_desc"`
385-
EditStrategies []editor.StrategyConfig `json:"edit_strategies,omitempty"`
386-
PinEditingStrategiesAtMessage string `json:"pin_editing_strategies_at_message,omitempty"`
380+
SessionID uuid.UUID `json:"session_id"`
381+
Limit int `json:"limit"`
382+
Cursor string `json:"cursor"`
383+
WithAssetPublicURL bool `json:"with_public_url"`
384+
AssetExpire time.Duration `json:"asset_expire"`
385+
TimeDesc bool `json:"time_desc"`
386+
EditStrategies []editor.StrategyConfig `json:"edit_strategies,omitempty"`
387+
// EditingTrigger holds optional trigger config for applying edit_strategies.
388+
EditingTrigger *EditingTrigger `json:"editing_trigger,omitempty"`
389+
PinEditingStrategiesAtMessage string `json:"pin_editing_strategies_at_message,omitempty"`
387390
// AutoTrim holds optional auto-trim config for get-messages.
388391
AutoTrim *AutoTrim `json:"auto_trim,omitempty"`
389392
}
390393

394+
// EditingTrigger defines trigger configuration for applying edit_strategies.
395+
// v0 supports only token_gte.
396+
type EditingTrigger struct {
397+
// TokenGte triggers edit strategies when the token count is greater than or equal to this value.
398+
TokenGte *int `json:"token_gte,omitempty"`
399+
}
400+
391401
// AutoTrim defines auto-trim configuration for get-messages.
392402
type AutoTrim struct {
393403
// TokenThreshold triggers trimming when token count is >= this value.
@@ -551,12 +561,51 @@ func (s *sessionService) GetMessages(ctx context.Context, in GetMessagesInput) (
551561

552562
// Apply edit strategies if provided (before format conversion)
553563
if len(in.EditStrategies) > 0 {
554-
result, err := editor.ApplyStrategiesWithPin(out.Items, in.EditStrategies, in.PinEditingStrategiesAtMessage)
555-
if err != nil {
556-
return nil, fmt.Errorf("failed to apply edit strategies: %w", err)
564+
applyEditStrategies := true
565+
if in.EditingTrigger != nil && in.EditingTrigger.TokenGte != nil && *in.EditingTrigger.TokenGte > 0 {
566+
// Evaluate trigger on the same editable prefix used by pin_editing_strategies_at_message.
567+
triggerMessages := out.Items
568+
effectivePin := ""
569+
if in.PinEditingStrategiesAtMessage != "" {
570+
pinIndex := -1
571+
for i := range out.Items {
572+
if out.Items[i].ID.String() == in.PinEditingStrategiesAtMessage {
573+
pinIndex = i
574+
break
575+
}
576+
}
577+
if pinIndex != -1 {
578+
effectivePin = in.PinEditingStrategiesAtMessage
579+
triggerMessages = out.Items[:pinIndex+1]
580+
}
581+
}
582+
583+
triggerTokens, err := tokenizer.CountMessagePartsTokens(ctx, triggerMessages)
584+
if err != nil {
585+
return nil, fmt.Errorf("failed to count tokens for editing_trigger session_id=%s: %w", in.SessionID, err)
586+
}
587+
588+
// OR semantics: v0 has a single trigger.
589+
applyEditStrategies = triggerTokens >= *in.EditingTrigger.TokenGte
590+
if !applyEditStrategies && len(out.Items) > 0 {
591+
if effectivePin != "" {
592+
out.EditAtMessageID = effectivePin
593+
} else {
594+
out.EditAtMessageID = out.Items[len(out.Items)-1].ID.String()
595+
}
596+
}
597+
}
598+
599+
if applyEditStrategies {
600+
result, err := editor.ApplyStrategiesWithPin(out.Items, in.EditStrategies, in.PinEditingStrategiesAtMessage)
601+
if err != nil {
602+
return nil, fmt.Errorf("failed to apply edit strategies: %w", err)
603+
}
604+
out.Items = result.Messages
605+
out.EditAtMessageID = result.EditAtMessageID
606+
} else if out.EditAtMessageID == "" && len(out.Items) > 0 {
607+
out.EditAtMessageID = out.Items[len(out.Items)-1].ID.String()
557608
}
558-
out.Items = result.Messages
559-
out.EditAtMessageID = result.EditAtMessageID
560609
} else if len(out.Items) > 0 {
561610
// No strategies, but still set EditAtMessageID to the last message
562611
out.EditAtMessageID = out.Items[len(out.Items)-1].ID.String()

0 commit comments

Comments
 (0)