Skip to content

Commit 610f38e

Browse files
committed
feat(api,sdk,docs): add keep_tools for editing strategies
1 parent 9e6603f commit 610f38e

8 files changed

Lines changed: 559 additions & 2 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Acontext is a platform for building AI agents with persistent context, observabi
1919
- CORE: Python + FastAPI + SQLAlchemy/PostgreSQL + pgvector + Redis + RabbitMQ + S3 + OpenAI/Anthropic + OpenTelemetry
2020

2121
## RULES
22+
### Find SDKs after modifying API
23+
When your task is about modify API, always look for the revelant SDKs and see it should be updated.
2224
### Find Document after modifying SDKs
2325
When your task is about implement/update SDKs at `src/client`, always look for the revelant documents and see it should be updated.
2426
DON't update openapi.json, or swagger.json/yaml. those are artifacts generated by programs, no need to manually edit them.

docs/engineering/editing.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ This strategy will replace the oldest tool results' content with a placeholder t
153153
**Parameters:**
154154
- `keep_recent_n_tool_results` (optional, default: 3): Number of most recent tool results to keep with original content
155155
- `tool_result_placeholder` (optional, default: "Done"): Custom text to replace old tool results with
156+
- `keep_tools` (optional): List of tool names that should never have their results removed. Tool results from these tools are always kept regardless of `keep_recent_n_tool_results`
156157

157158
**Example Output:**
158159

@@ -237,6 +238,7 @@ This is particularly useful when you have many tool calls in your session histor
237238

238239
**Parameters:**
239240
- `keep_recent_n_tool_calls` (optional, default: 3): Number of most recent tool calls to keep with full parameters
241+
- `keep_tools` (optional): List of tool names that should never have their parameters removed. Tool calls for these tools always keep their full parameters regardless of `keep_recent_n_tool_calls`
240242

241243
**How it works:**
242244
- Keeps the most recent N tool calls with their original parameters

src/client/acontext-py/src/acontext/types/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ class RemoveToolResultParams(TypedDict, total=False):
1313
Defaults to 3 if not specified.
1414
tool_result_placeholder: Custom text to replace old tool results with.
1515
Defaults to "Done" if not specified.
16+
keep_tools: List of tool names that should never have their results removed.
17+
Tool results from these tools are always kept regardless of keep_recent_n_tool_results.
1618
"""
1719

1820
keep_recent_n_tool_results: NotRequired[int]
1921
tool_result_placeholder: NotRequired[str]
22+
keep_tools: NotRequired[list[str]]
2023

2124

2225
class RemoveToolResultStrategy(TypedDict):
@@ -36,9 +39,12 @@ class RemoveToolCallParamsParams(TypedDict, total=False):
3639
Attributes:
3740
keep_recent_n_tool_calls: Number of most recent tool calls to keep with full parameters.
3841
Defaults to 3 if not specified.
42+
keep_tools: List of tool names that should never have their parameters removed.
43+
Tool calls for these tools always keep their full parameters regardless of keep_recent_n_tool_calls.
3944
"""
4045

4146
keep_recent_n_tool_calls: NotRequired[int]
47+
keep_tools: NotRequired[list[str]]
4248

4349

4450
class RemoveToolCallParamsStrategy(TypedDict):

src/client/acontext-ts/src/types/session.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ export const RemoveToolResultParamsSchema = z.object({
162162
* @default "Done"
163163
*/
164164
tool_result_placeholder: z.string().optional(),
165+
166+
/**
167+
* List of tool names that should never have their results removed.
168+
* Tool results from these tools are always kept regardless of keep_recent_n_tool_results.
169+
*/
170+
keep_tools: z.array(z.string()).optional(),
165171
});
166172

167173
export type RemoveToolResultParams = z.infer<typeof RemoveToolResultParamsSchema>;
@@ -175,6 +181,12 @@ export const RemoveToolCallParamsParamsSchema = z.object({
175181
* @default 3
176182
*/
177183
keep_recent_n_tool_calls: z.number().optional(),
184+
185+
/**
186+
* List of tool names that should never have their parameters removed.
187+
* Tool calls for these tools always keep their full parameters regardless of keep_recent_n_tool_calls.
188+
*/
189+
keep_tools: z.array(z.string()).optional(),
178190
});
179191
export type RemoveToolCallParamsParams = z.infer<typeof RemoveToolCallParamsParamsSchema>;
180192

src/server/api/go/internal/pkg/editor/strategy_remove_tool_call_params.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
// RemoveToolCallParamsStrategy removes parameters from old tool-call parts
1010
type RemoveToolCallParamsStrategy struct {
1111
KeepRecentN int
12+
KeepTools []string // Tool names that should never have their parameters removed
1213
}
1314

1415
// Name returns the strategy name
@@ -18,12 +19,19 @@ func (s *RemoveToolCallParamsStrategy) Name() string {
1819

1920
// Apply removes input parameters from old tool-call parts
2021
// Keeps the most recent N tool-call parts with their original parameters
22+
// Also keeps parameters for tools listed in KeepTools
2123
func (s *RemoveToolCallParamsStrategy) Apply(messages []model.Message) ([]model.Message, error) {
2224
if s.KeepRecentN < 0 {
2325
return nil, fmt.Errorf("keep_recent_n_tool_calls must be >= 0, got %d", s.KeepRecentN)
2426
}
2527

26-
// Collect all tool-call parts with their positions
28+
// Build a set of tool names to keep for O(1) lookup
29+
keepToolsSet := make(map[string]bool)
30+
for _, toolName := range s.KeepTools {
31+
keepToolsSet[toolName] = true
32+
}
33+
34+
// Collect all tool-call parts with their positions, excluding those in KeepTools
2735
type toolCallPosition struct {
2836
messageIdx int
2937
partIdx int
@@ -33,6 +41,15 @@ func (s *RemoveToolCallParamsStrategy) Apply(messages []model.Message) ([]model.
3341
for msgIdx, msg := range messages {
3442
for partIdx, part := range msg.Parts {
3543
if part.Type == "tool-call" {
44+
// Check if this tool call should be kept based on KeepTools
45+
if part.Meta != nil {
46+
if toolName, ok := part.Meta["name"].(string); ok {
47+
if keepToolsSet[toolName] {
48+
// Skip this tool call - its parameters should always be kept
49+
continue
50+
}
51+
}
52+
}
3653
toolCallPositions = append(toolCallPositions, toolCallPosition{
3754
messageIdx: msgIdx,
3855
partIdx: partIdx,
@@ -75,7 +92,26 @@ func createRemoveToolCallParamsStrategy(params map[string]interface{}) (EditStra
7592
}
7693
}
7794

95+
// Get keep_tools list (tool names that should never have their parameters removed)
96+
var keepTools []string
97+
if keepToolsValue, ok := params["keep_tools"]; ok {
98+
if keepToolsArr, ok := keepToolsValue.([]interface{}); ok {
99+
for _, v := range keepToolsArr {
100+
if toolName, ok := v.(string); ok {
101+
keepTools = append(keepTools, toolName)
102+
} else {
103+
return nil, fmt.Errorf("keep_tools must be an array of strings, got element of type %T", v)
104+
}
105+
}
106+
} else if keepToolsStrArr, ok := keepToolsValue.([]string); ok {
107+
keepTools = keepToolsStrArr
108+
} else {
109+
return nil, fmt.Errorf("keep_tools must be an array of strings, got %T", keepToolsValue)
110+
}
111+
}
112+
78113
return &RemoveToolCallParamsStrategy{
79114
KeepRecentN: keepRecentNInt,
115+
KeepTools: keepTools,
80116
}, nil
81117
}

src/server/api/go/internal/pkg/editor/strategy_remove_tool_call_params_test.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,236 @@ func TestRemoveToolCallParamsStrategy_Apply(t *testing.T) {
170170
assert.NoError(t, err)
171171
assert.Nil(t, result[0].Parts[0].Meta)
172172
})
173+
174+
t.Run("keep_tools prevents removal of specified tool call params", func(t *testing.T) {
175+
messages := []model.Message{
176+
{
177+
Parts: []model.Part{
178+
{
179+
Type: "tool-call",
180+
Meta: map[string]any{
181+
"id": "call_1",
182+
"name": "important_tool",
183+
"arguments": `{"key": "important_value"}`,
184+
},
185+
},
186+
},
187+
},
188+
{
189+
Parts: []model.Part{
190+
{
191+
Type: "tool-call",
192+
Meta: map[string]any{
193+
"id": "call_2",
194+
"name": "regular_tool",
195+
"arguments": `{"key": "regular_value"}`,
196+
},
197+
},
198+
},
199+
},
200+
{
201+
Parts: []model.Part{
202+
{
203+
Type: "tool-call",
204+
Meta: map[string]any{
205+
"id": "call_3",
206+
"name": "important_tool",
207+
"arguments": `{"key": "another_important_value"}`,
208+
},
209+
},
210+
},
211+
},
212+
}
213+
214+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 0, KeepTools: []string{"important_tool"}}
215+
result, err := strategy.Apply(messages)
216+
217+
assert.NoError(t, err)
218+
// important_tool calls should keep their arguments
219+
assert.Equal(t, `{"key": "important_value"}`, result[0].Parts[0].Meta["arguments"])
220+
assert.Equal(t, `{"key": "another_important_value"}`, result[2].Parts[0].Meta["arguments"])
221+
// regular_tool should have arguments cleared
222+
assert.Equal(t, "{}", result[1].Parts[0].Meta["arguments"])
223+
})
224+
225+
t.Run("keep_tools with keep_recent_n", func(t *testing.T) {
226+
messages := []model.Message{
227+
{
228+
Parts: []model.Part{
229+
{
230+
Type: "tool-call",
231+
Meta: map[string]any{
232+
"id": "call_1",
233+
"name": "regular_tool",
234+
"arguments": `{"key": "old_regular"}`,
235+
},
236+
},
237+
},
238+
},
239+
{
240+
Parts: []model.Part{
241+
{
242+
Type: "tool-call",
243+
Meta: map[string]any{
244+
"id": "call_2",
245+
"name": "important_tool",
246+
"arguments": `{"key": "important"}`,
247+
},
248+
},
249+
},
250+
},
251+
{
252+
Parts: []model.Part{
253+
{
254+
Type: "tool-call",
255+
Meta: map[string]any{
256+
"id": "call_3",
257+
"name": "regular_tool",
258+
"arguments": `{"key": "recent_regular"}`,
259+
},
260+
},
261+
},
262+
},
263+
}
264+
265+
// Keep 1 recent regular tool call + all important_tool calls
266+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 1, KeepTools: []string{"important_tool"}}
267+
result, err := strategy.Apply(messages)
268+
269+
assert.NoError(t, err)
270+
// Old regular call should have arguments cleared
271+
assert.Equal(t, "{}", result[0].Parts[0].Meta["arguments"])
272+
// important_tool should keep arguments
273+
assert.Equal(t, `{"key": "important"}`, result[1].Parts[0].Meta["arguments"])
274+
// Recent regular call should keep arguments (within keep_recent_n)
275+
assert.Equal(t, `{"key": "recent_regular"}`, result[2].Parts[0].Meta["arguments"])
276+
})
277+
278+
t.Run("keep_tools with multiple tool names", func(t *testing.T) {
279+
messages := []model.Message{
280+
{
281+
Parts: []model.Part{
282+
{
283+
Type: "tool-call",
284+
Meta: map[string]any{
285+
"id": "call_1",
286+
"name": "tool_a",
287+
"arguments": `{"key": "a"}`,
288+
},
289+
},
290+
},
291+
},
292+
{
293+
Parts: []model.Part{
294+
{
295+
Type: "tool-call",
296+
Meta: map[string]any{
297+
"id": "call_2",
298+
"name": "tool_b",
299+
"arguments": `{"key": "b"}`,
300+
},
301+
},
302+
},
303+
},
304+
{
305+
Parts: []model.Part{
306+
{
307+
Type: "tool-call",
308+
Meta: map[string]any{
309+
"id": "call_3",
310+
"name": "tool_c",
311+
"arguments": `{"key": "c"}`,
312+
},
313+
},
314+
},
315+
},
316+
}
317+
318+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 0, KeepTools: []string{"tool_a", "tool_c"}}
319+
result, err := strategy.Apply(messages)
320+
321+
assert.NoError(t, err)
322+
// tool_a and tool_c should keep arguments
323+
assert.Equal(t, `{"key": "a"}`, result[0].Parts[0].Meta["arguments"])
324+
assert.Equal(t, `{"key": "c"}`, result[2].Parts[0].Meta["arguments"])
325+
// tool_b should have arguments cleared
326+
assert.Equal(t, "{}", result[1].Parts[0].Meta["arguments"])
327+
})
328+
}
329+
330+
func TestCreateRemoveToolCallParamsStrategy(t *testing.T) {
331+
t.Run("create with keep_tools parameter", func(t *testing.T) {
332+
config := StrategyConfig{
333+
Type: "remove_tool_call_params",
334+
Params: map[string]interface{}{
335+
"keep_tools": []interface{}{"tool1", "tool2"},
336+
},
337+
}
338+
339+
strategy, err := CreateStrategy(config)
340+
341+
assert.NoError(t, err)
342+
rtcp, ok := strategy.(*RemoveToolCallParamsStrategy)
343+
assert.True(t, ok)
344+
assert.Equal(t, []string{"tool1", "tool2"}, rtcp.KeepTools)
345+
})
346+
347+
t.Run("invalid keep_tools type returns error", func(t *testing.T) {
348+
config := StrategyConfig{
349+
Type: "remove_tool_call_params",
350+
Params: map[string]interface{}{
351+
"keep_tools": "not_an_array",
352+
},
353+
}
354+
355+
_, err := CreateStrategy(config)
356+
357+
assert.Error(t, err)
358+
assert.Contains(t, err.Error(), "keep_tools must be an array of strings")
359+
})
360+
361+
t.Run("invalid keep_tools element type returns error", func(t *testing.T) {
362+
config := StrategyConfig{
363+
Type: "remove_tool_call_params",
364+
Params: map[string]interface{}{
365+
"keep_tools": []interface{}{"valid", 123},
366+
},
367+
}
368+
369+
_, err := CreateStrategy(config)
370+
371+
assert.Error(t, err)
372+
assert.Contains(t, err.Error(), "keep_tools must be an array of strings")
373+
})
374+
375+
t.Run("empty keep_tools is valid", func(t *testing.T) {
376+
config := StrategyConfig{
377+
Type: "remove_tool_call_params",
378+
Params: map[string]interface{}{
379+
"keep_tools": []interface{}{},
380+
},
381+
}
382+
383+
strategy, err := CreateStrategy(config)
384+
385+
assert.NoError(t, err)
386+
rtcp, ok := strategy.(*RemoveToolCallParamsStrategy)
387+
assert.True(t, ok)
388+
assert.Empty(t, rtcp.KeepTools)
389+
})
390+
391+
t.Run("default values when no params provided", func(t *testing.T) {
392+
config := StrategyConfig{
393+
Type: "remove_tool_call_params",
394+
Params: map[string]interface{}{},
395+
}
396+
397+
strategy, err := CreateStrategy(config)
398+
399+
assert.NoError(t, err)
400+
rtcp, ok := strategy.(*RemoveToolCallParamsStrategy)
401+
assert.True(t, ok)
402+
assert.Equal(t, 3, rtcp.KeepRecentN)
403+
assert.Nil(t, rtcp.KeepTools)
404+
})
173405
}

0 commit comments

Comments
 (0)