Skip to content

Commit 5b3111f

Browse files
authored
feat: Add remove_tool_call_params edit strategy (#84)
* feat: Add remove_tool_call_params edit strategy Implements #73 - adds new context editing strategy to remove parameters from old tool-call parts, keeping only recent N with full params. changes: - Go API: New strategy in pkg/editor with priority 2 - Python SDK: added RemoveToolCallParamsStrategy type hints - TypeSCript SDK: Added RemoveToolCallParamsStrategySChema - Tests: Added unit test for the new strategy TOol call ID and Name remain intact so tool-results can reference them easily Parameters replace with empty JSON '{}' Tested and working via API endpoint * test: Add boundary casetests and modernize loop - Add test or edge cases: zero value, negative value, nil Meta, mixed parts, no tool calls - Modernize loop to use range over int
1 parent cf9081d commit 5b3111f

File tree

5 files changed

+314
-1
lines changed

5 files changed

+314
-1
lines changed

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ class RemoveToolResultStrategy(TypedDict):
3030
params: RemoveToolResultParams
3131

3232

33+
class RemoveToolCallParamsParams(TypedDict, total=False):
34+
"""Parameters for the remove_tool_call_params edit strategy.
35+
36+
Attributes:
37+
keep_recent_n_tool_calls: Number of most recent tool calls to keep with full parameters.
38+
Defaults to 3 if not specified.
39+
"""
40+
41+
keep_recent_n_tool_calls: NotRequired[int]
42+
43+
44+
45+
class RemoveToolCallParamsStrategy(TypedDict):
46+
"""Edit strategy to remove parameters from old tool-call parts.
47+
48+
Keeps the most recent N tool calls with full parameters, replacing older
49+
tool call arguments with empty JSON "{}". The tool call ID and name remain
50+
intact so tool-results can still reference them.
51+
52+
Example:
53+
{"type": "remove_tool_call_params", "params": {"keep_recent_n_tool_calls": 5}}
54+
"""
55+
56+
type: Literal["remove_tool_call_params"]
57+
params: RemoveToolCallParamsParams
58+
59+
60+
3361
class TokenLimitParams(TypedDict):
3462
"""Parameters for the token_limit edit strategy.
3563
@@ -59,7 +87,7 @@ class TokenLimitStrategy(TypedDict):
5987

6088
# Union type for all edit strategies
6189
# When adding new strategies, add them to this Union: EditStrategy = Union[RemoveToolResultStrategy, OtherStrategy, ...]
62-
EditStrategy = Union[RemoveToolResultStrategy, TokenLimitStrategy]
90+
EditStrategy = Union[RemoveToolResultStrategy, RemoveToolCallParamsStrategy, TokenLimitStrategy]
6391

6492

6593
class Asset(BaseModel):

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,33 @@ export const RemoveToolResultParamsSchema = z.object({
145145

146146
export type RemoveToolResultParams = z.infer<typeof RemoveToolResultParamsSchema>;
147147

148+
/**
149+
* Parameters for the remove_tool_call_params edit strategy.
150+
*/
151+
export const RemoveToolCallParamsParamsSchema = z.object({
152+
/**
153+
* Number of most recent tool calls to keep with full parameters.
154+
* @default 3
155+
*/
156+
keep_recent_n_tool_calls: z.number().optional(),
157+
});
158+
export type RemoveToolCallParamsParams = z.infer<typeof RemoveToolCallParamsParamsSchema>;
159+
160+
/**
161+
* Edit strategy to remove parameters from old tool-call parts.
162+
*
163+
* Keeps the most recent N tool calls with full parameters, replacing older
164+
* tool call arguments with empty JSON "{}". The tool call ID and name remain
165+
* intact so tool-results can still reference them.
166+
*
167+
* Example: { type: 'remove_tool_call_params', params: { keep_recent_n_tool_calls: 5 } }
168+
*/
169+
export const RemoveToolCallParamsStrategySchema = z.object({
170+
type: z.literal('remove_tool_call_params'),
171+
params: RemoveToolCallParamsParamsSchema,
172+
});
173+
export type RemoveToolCallParamsStrategy = z.infer<typeof RemoveToolCallParamsStrategySchema>;
174+
148175
/**
149176
* Edit strategy to replace old tool results with placeholder text.
150177
*
@@ -193,6 +220,7 @@ export type TokenLimitStrategy = z.infer<typeof TokenLimitStrategySchema>;
193220
*/
194221
export const EditStrategySchema = z.union([
195222
RemoveToolResultStrategySchema,
223+
RemoveToolCallParamsStrategySchema,
196224
TokenLimitStrategySchema,
197225
]);
198226

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ func CreateStrategy(config StrategyConfig) (EditStrategy, error) {
2424
switch config.Type {
2525
case "remove_tool_result":
2626
return createRemoveToolResultStrategy(config.Params)
27+
case "remove_tool_call_params":
28+
return createRemoveToolCallParamsStrategy(config.Params)
2729
case "token_limit":
2830
return createTokenLimitStrategy(config.Params)
2931
default:
@@ -38,6 +40,8 @@ func getStrategyPriority(strategyType string) int {
3840
switch strategyType {
3941
case "remove_tool_result":
4042
return 1 // Content reduction strategies go first
43+
case "remove_tool_call_params":
44+
return 2
4145
case "token_limit":
4246
return 100 // Token limit always goes last
4347
default:
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package editor
2+
3+
import (
4+
"fmt"
5+
"github.com/memodb-io/Acontext/internal/modules/model"
6+
)
7+
8+
// RemoveToolCallParamsStrategy removes parameters from old tool-call parts
9+
type RemoveToolCallParamsStrategy struct {
10+
KeepRecentN int
11+
}
12+
13+
// Name returns the strategy name
14+
func (s *RemoveToolCallParamsStrategy) Name() string {
15+
return "remove_tool_call_params"
16+
}
17+
18+
// Apply removes input parameters from old tool-call parts
19+
// Keeps the most recent N tool-call parts with their original parameters
20+
func (s *RemoveToolCallParamsStrategy) Apply(messages []model.Message) ([]model.Message, error) {
21+
if s.KeepRecentN < 0 {
22+
return nil, fmt.Errorf("keep_recent_n_tool_calls must be >= 0, got %d", s.KeepRecentN)
23+
}
24+
25+
// Collect all tool-call parts with their positions
26+
type toolCallPosition struct {
27+
messageIdx int
28+
partIdx int
29+
}
30+
var toolCallPositions []toolCallPosition
31+
32+
for msgIdx, msg := range messages {
33+
for partIdx, part := range msg.Parts {
34+
if part.Type == "tool-call" {
35+
toolCallPositions = append(toolCallPositions, toolCallPosition{
36+
messageIdx: msgIdx,
37+
partIdx: partIdx,
38+
})
39+
}
40+
}
41+
}
42+
43+
// Calculate how many to modify
44+
totalToolCalls := len(toolCallPositions)
45+
if totalToolCalls <= s.KeepRecentN {
46+
return messages, nil
47+
}
48+
49+
numToModify := totalToolCalls - s.KeepRecentN
50+
51+
// Remove arguments from the oldest tool-call parts
52+
for i := range numToModify {
53+
pos := toolCallPositions[i]
54+
if messages[pos.messageIdx].Parts[pos.partIdx].Meta != nil {
55+
messages[pos.messageIdx].Parts[pos.partIdx].Meta["arguments"] = "{}"
56+
}
57+
}
58+
59+
return messages, nil
60+
}
61+
62+
// createRemoveToolCallParamsStrategy creates a RemoveToolCallParamsStrategy from config params
63+
func createRemoveToolCallParamsStrategy(params map[string]interface{}) (EditStrategy, error) {
64+
keepRecentNInt := 3
65+
66+
if keepRecentN, ok := params["keep_recent_n_tool_calls"]; ok {
67+
switch v := keepRecentN.(type) {
68+
case float64:
69+
keepRecentNInt = int(v)
70+
case int:
71+
keepRecentNInt = v
72+
default:
73+
return nil, fmt.Errorf("keep_recent_n_tool_calls must be an integer, got %T", keepRecentN)
74+
}
75+
}
76+
77+
return &RemoveToolCallParamsStrategy{
78+
KeepRecentN: keepRecentNInt,
79+
}, nil
80+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package editor
2+
3+
import (
4+
"testing"
5+
6+
"github.com/memodb-io/Acontext/internal/modules/model"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestRemoveToolCallParamsStrategy_Apply(t *testing.T) {
11+
t.Run("removes parameters from old tool calls", func(t *testing.T) {
12+
messages := []model.Message{
13+
{
14+
Parts: []model.Part{
15+
{
16+
Type: "tool-call",
17+
Meta: map[string]any{
18+
"id": "call_1",
19+
"name": "search",
20+
"arguments": `{"query": "old search"}`,
21+
},
22+
},
23+
},
24+
},
25+
{
26+
Parts: []model.Part{
27+
{
28+
Type: "tool-call",
29+
Meta: map[string]any{
30+
"id": "call_2",
31+
"name": "search",
32+
"arguments": `{"query": "recent search"}`,
33+
},
34+
},
35+
},
36+
},
37+
}
38+
39+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 1}
40+
result, err := strategy.Apply(messages)
41+
42+
assert.NoError(t, err)
43+
assert.Equal(t, "{}", result[0].Parts[0].Meta["arguments"])
44+
assert.Equal(t, `{"query": "recent search"}`, result[1].Parts[0].Meta["arguments"])
45+
})
46+
47+
t.Run("keeps all when under limit", func(t *testing.T) {
48+
messages := []model.Message{
49+
{
50+
Parts: []model.Part{
51+
{
52+
Type: "tool-call",
53+
Meta: map[string]any{
54+
"id": "call_1",
55+
"name": "search",
56+
"arguments": `{"query": "test"}`,
57+
},
58+
},
59+
},
60+
},
61+
}
62+
63+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 3}
64+
result, err := strategy.Apply(messages)
65+
66+
assert.NoError(t, err)
67+
assert.Equal(t, `{"query": "test"}`, result[0].Parts[0].Meta["arguments"])
68+
})
69+
70+
t.Run("removes all when keep_recent_n is zero", func(t *testing.T) {
71+
messages := []model.Message{
72+
{
73+
Parts: []model.Part{
74+
{
75+
Type: "tool-call",
76+
Meta: map[string]any{
77+
"id": "call_1",
78+
"name": "search",
79+
"arguments": `{"query": "test"}`,
80+
},
81+
},
82+
},
83+
},
84+
}
85+
86+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 0}
87+
result, err := strategy.Apply(messages)
88+
89+
assert.NoError(t, err)
90+
assert.Equal(t, "{}", result[0].Parts[0].Meta["arguments"])
91+
})
92+
93+
t.Run("returns error for negative keep_recent_n", func(t *testing.T) {
94+
messages := []model.Message{}
95+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: -1}
96+
_, err := strategy.Apply(messages)
97+
98+
assert.Error(t, err)
99+
assert.Contains(t, err.Error(), "must be >= 0")
100+
})
101+
102+
t.Run("handles messages with no tool calls", func(t *testing.T) {
103+
messages := []model.Message{
104+
{
105+
Parts: []model.Part{
106+
{Type: "text", Text: "hello"},
107+
},
108+
},
109+
}
110+
111+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 1}
112+
result, err := strategy.Apply(messages)
113+
114+
assert.NoError(t, err)
115+
assert.Equal(t, messages, result)
116+
})
117+
118+
t.Run("handles mixed part types", func(t *testing.T) {
119+
messages := []model.Message{
120+
{
121+
Parts: []model.Part{
122+
{Type: "text", Text: "hello"},
123+
{
124+
Type: "tool-call",
125+
Meta: map[string]any{
126+
"id": "call_1",
127+
"name": "search",
128+
"arguments": `{"query": "old"}`,
129+
},
130+
},
131+
},
132+
},
133+
{
134+
Parts: []model.Part{
135+
{
136+
Type: "tool-call",
137+
Meta: map[string]any{
138+
"id": "call_2",
139+
"name": "search",
140+
"arguments": `{"query": "new"}`,
141+
},
142+
},
143+
},
144+
},
145+
}
146+
147+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 1}
148+
result, err := strategy.Apply(messages)
149+
150+
assert.NoError(t, err)
151+
assert.Equal(t, "{}", result[0].Parts[1].Meta["arguments"])
152+
assert.Equal(t, `{"query": "new"}`, result[1].Parts[0].Meta["arguments"])
153+
})
154+
155+
t.Run("handles tool call with nil meta gracefully", func(t *testing.T) {
156+
messages := []model.Message{
157+
{
158+
Parts: []model.Part{
159+
{
160+
Type: "tool-call",
161+
Meta: nil,
162+
},
163+
},
164+
},
165+
}
166+
167+
strategy := &RemoveToolCallParamsStrategy{KeepRecentN: 0}
168+
result, err := strategy.Apply(messages)
169+
170+
assert.NoError(t, err)
171+
assert.Nil(t, result[0].Parts[0].Meta)
172+
})
173+
}

0 commit comments

Comments
 (0)