fix: parallel tool calling misc fixes (#85)
This commit is contained in:
@@ -220,9 +220,18 @@ export async function createAgent(
|
||||
enable_sleeptime: enableSleeptime,
|
||||
});
|
||||
|
||||
// Apply updateArgs if provided (e.g., reasoningEffort, contextWindow, etc.)
|
||||
// Apply updateArgs if provided (e.g., reasoningEffort, verbosity, etc.)
|
||||
// Skip if updateArgs only contains context_window (already set in create)
|
||||
if (updateArgs && Object.keys(updateArgs).length > 0) {
|
||||
await updateAgentLLMConfig(agent.id, modelHandle, updateArgs);
|
||||
const { context_window, ...otherArgs } = updateArgs;
|
||||
if (Object.keys(otherArgs).length > 0) {
|
||||
await updateAgentLLMConfig(
|
||||
agent.id,
|
||||
modelHandle,
|
||||
otherArgs,
|
||||
true, // preserve parallel_tool_calls
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Always retrieve the agent to ensure we get the full state with populated memory blocks
|
||||
|
||||
@@ -14,36 +14,42 @@ import { getClient } from "./client";
|
||||
* @param agentId - The agent ID
|
||||
* @param modelHandle - The model handle (e.g., "anthropic/claude-sonnet-4-5-20250929")
|
||||
* @param updateArgs - Additional LLM config args (contextWindow, reasoningEffort, verbosity, etc.)
|
||||
* @param preserveParallelToolCalls - If true, preserves the parallel_tool_calls setting when updating the model
|
||||
* @returns The updated LLM configuration from the server
|
||||
*/
|
||||
export async function updateAgentLLMConfig(
|
||||
agentId: string,
|
||||
modelHandle: string,
|
||||
updateArgs?: Record<string, unknown>,
|
||||
preserveParallelToolCalls?: boolean,
|
||||
): Promise<LlmConfig> {
|
||||
const client = await getClient();
|
||||
|
||||
// Step 1: Update model (top-level field)
|
||||
await client.agents.modify(agentId, { model: modelHandle });
|
||||
// Get current agent to preserve parallel_tool_calls if requested
|
||||
const currentAgent = await client.agents.retrieve(agentId);
|
||||
const originalParallelToolCalls = preserveParallelToolCalls
|
||||
? (currentAgent.llm_config?.parallel_tool_calls ?? undefined)
|
||||
: undefined;
|
||||
|
||||
// Step 2: Get updated agent to retrieve current llm_config
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
let finalConfig = agent.llm_config;
|
||||
// Strategy: Do everything in ONE modify call via llm_config
|
||||
// This avoids the backend resetting parallel_tool_calls when we update the model
|
||||
const updatedLlmConfig = {
|
||||
...currentAgent.llm_config,
|
||||
...updateArgs,
|
||||
// Explicitly preserve parallel_tool_calls
|
||||
...(originalParallelToolCalls !== undefined && {
|
||||
parallel_tool_calls: originalParallelToolCalls,
|
||||
}),
|
||||
} as LlmConfig;
|
||||
|
||||
// Step 3: If we have updateArgs, merge them into llm_config and patch again
|
||||
if (updateArgs && Object.keys(updateArgs).length > 0) {
|
||||
const updatedLlmConfig = {
|
||||
...finalConfig,
|
||||
...updateArgs,
|
||||
} as LlmConfig;
|
||||
await client.agents.modify(agentId, { llm_config: updatedLlmConfig });
|
||||
await client.agents.modify(agentId, {
|
||||
llm_config: updatedLlmConfig,
|
||||
parallel_tool_calls: originalParallelToolCalls,
|
||||
});
|
||||
|
||||
// Retrieve final state
|
||||
const finalAgent = await client.agents.retrieve(agentId);
|
||||
finalConfig = finalAgent.llm_config;
|
||||
}
|
||||
|
||||
return finalConfig;
|
||||
// Retrieve and return final state
|
||||
const finalAgent = await client.agents.retrieve(agentId);
|
||||
return finalAgent.llm_config;
|
||||
}
|
||||
|
||||
export interface LinkResult {
|
||||
|
||||
@@ -91,14 +91,21 @@ export async function drainStream(
|
||||
// tool_call_message = auto-executed server-side (e.g., web_search)
|
||||
// approval_request_message = needs user approval (e.g., Bash)
|
||||
if (chunk.message_type === "approval_request_message") {
|
||||
// Use deprecated tool_call or new tool_calls array
|
||||
const toolCall =
|
||||
chunk.tool_call ||
|
||||
(Array.isArray(chunk.tool_calls) && chunk.tool_calls.length > 0
|
||||
? chunk.tool_calls[0]
|
||||
: null);
|
||||
// console.log(
|
||||
// "[drainStream] approval_request_message chunk:",
|
||||
// JSON.stringify(chunk, null, 2),
|
||||
// );
|
||||
|
||||
// Normalize tool calls: support both legacy tool_call and new tool_calls array
|
||||
const toolCalls = Array.isArray(chunk.tool_calls)
|
||||
? chunk.tool_calls
|
||||
: chunk.tool_call
|
||||
? [chunk.tool_call]
|
||||
: [];
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
if (!toolCall?.tool_call_id) continue; // strict: require id
|
||||
|
||||
if (toolCall?.tool_call_id) {
|
||||
// Get or create entry for this tool_call_id
|
||||
const existing = pendingApprovals.get(toolCall.tool_call_id) || {
|
||||
toolCallId: toolCall.tool_call_id,
|
||||
@@ -149,7 +156,13 @@ export async function drainStream(
|
||||
|
||||
if (stopReason === "requires_approval") {
|
||||
// Convert map to array, filtering out incomplete entries
|
||||
approvals = Array.from(pendingApprovals.values()).filter(
|
||||
const allPending = Array.from(pendingApprovals.values());
|
||||
// console.log(
|
||||
// "[drainStream] All pending approvals before filter:",
|
||||
// JSON.stringify(allPending, null, 2),
|
||||
// );
|
||||
|
||||
approvals = allPending.filter(
|
||||
(a) => a.toolCallId && a.toolName && a.toolArgs,
|
||||
);
|
||||
|
||||
@@ -157,6 +170,7 @@ export async function drainStream(
|
||||
console.error(
|
||||
"[drainStream] No valid approvals collected despite requires_approval stop reason",
|
||||
);
|
||||
console.error("[drainStream] Pending approvals map:", allPending);
|
||||
} else {
|
||||
// Set legacy singular field for backward compatibility
|
||||
approval = approvals[0] || null;
|
||||
|
||||
132
src/headless.ts
132
src/headless.ts
@@ -406,7 +406,7 @@ export async function handleHeadlessCommand(
|
||||
chunk.message_type === "approval_request_message";
|
||||
let shouldOutputChunk = true;
|
||||
|
||||
// Track approval requests
|
||||
// Track approval requests (stream-aware: accumulate by tool_call_id)
|
||||
if (isApprovalRequest) {
|
||||
const chunkWithTools = chunk as typeof chunk & {
|
||||
tool_call?: {
|
||||
@@ -428,77 +428,77 @@ export async function handleHeadlessCommand(
|
||||
: [];
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
if (toolCall?.tool_call_id && toolCall?.name) {
|
||||
const id = toolCall.tool_call_id;
|
||||
_lastApprovalId = id;
|
||||
const id = toolCall?.tool_call_id;
|
||||
if (!id) continue; // remain strict: do not invent ids
|
||||
|
||||
// Prefer the most complete args we have seen so far; concatenate deltas
|
||||
const prev = approvalRequests.get(id);
|
||||
const base = prev && prev.args !== "{}" ? prev.args : "";
|
||||
const incomingArgs =
|
||||
toolCall.arguments && toolCall.arguments.trim().length > 0
|
||||
? `${base}${toolCall.arguments}`
|
||||
: base || "{}";
|
||||
_lastApprovalId = id;
|
||||
|
||||
approvalRequests.set(id, {
|
||||
toolName: toolCall.name,
|
||||
args: incomingArgs,
|
||||
});
|
||||
// Concatenate argument deltas; do not inject placeholder JSON
|
||||
const prev = approvalRequests.get(id);
|
||||
const base = prev?.args ?? "";
|
||||
const incomingArgs =
|
||||
toolCall?.arguments && toolCall.arguments.trim().length > 0
|
||||
? base + toolCall.arguments
|
||||
: base;
|
||||
|
||||
// Keep an up-to-date approvals array for downstream handling
|
||||
// Update existing approval if present, otherwise add new one
|
||||
const existingIndex = approvals.findIndex(
|
||||
(a) => a.toolCallId === id,
|
||||
// Preserve previously seen name; set if provided in this chunk
|
||||
const nextName = toolCall?.name || prev?.toolName || "";
|
||||
approvalRequests.set(id, {
|
||||
toolName: nextName,
|
||||
args: incomingArgs,
|
||||
});
|
||||
|
||||
// Keep an up-to-date approvals array for downstream handling
|
||||
// Update existing approval if present, otherwise add new one
|
||||
const existingIndex = approvals.findIndex(
|
||||
(a) => a.toolCallId === id,
|
||||
);
|
||||
const approvalObj = {
|
||||
toolCallId: id,
|
||||
toolName: nextName,
|
||||
toolArgs: incomingArgs,
|
||||
};
|
||||
if (existingIndex >= 0) {
|
||||
approvals[existingIndex] = approvalObj;
|
||||
} else {
|
||||
approvals.push(approvalObj);
|
||||
}
|
||||
|
||||
// Check if this approval will be auto-approved. Dedup per tool_call_id
|
||||
if (!autoApprovalEmitted.has(id) && nextName) {
|
||||
const parsedArgs = safeJsonParseOr<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null>(incomingArgs || "{}", null);
|
||||
const permission = await checkToolPermission(
|
||||
nextName,
|
||||
parsedArgs || {},
|
||||
);
|
||||
const approvalObj = {
|
||||
toolCallId: id,
|
||||
toolName: toolCall.name,
|
||||
toolArgs: incomingArgs,
|
||||
};
|
||||
if (existingIndex >= 0) {
|
||||
approvals[existingIndex] = approvalObj;
|
||||
} else {
|
||||
approvals.push(approvalObj);
|
||||
}
|
||||
|
||||
// Check if this approval will be auto-approved. Dedup per tool_call_id
|
||||
if (!autoApprovalEmitted.has(id)) {
|
||||
const parsedArgs = safeJsonParseOr<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null>(incomingArgs || "{}", null);
|
||||
const permission = await checkToolPermission(
|
||||
toolCall.name,
|
||||
parsedArgs || {},
|
||||
if (permission.decision === "allow" && parsedArgs) {
|
||||
// Only emit auto_approval if we already have all required params
|
||||
const { getToolSchema } = await import("./tools/manager");
|
||||
const schema = getToolSchema(nextName);
|
||||
const required =
|
||||
(schema?.input_schema?.required as string[] | undefined) ||
|
||||
[];
|
||||
const missing = required.filter(
|
||||
(key) =>
|
||||
!(key in parsedArgs) ||
|
||||
String((parsedArgs as Record<string, unknown>)[key] ?? "")
|
||||
.length === 0,
|
||||
);
|
||||
if (permission.decision === "allow" && parsedArgs) {
|
||||
// Only emit auto_approval if we already have all required params
|
||||
const { getToolSchema } = await import("./tools/manager");
|
||||
const schema = getToolSchema(toolCall.name);
|
||||
const required =
|
||||
(schema?.input_schema?.required as
|
||||
| string[]
|
||||
| undefined) || [];
|
||||
const missing = required.filter(
|
||||
(key) =>
|
||||
!(key in parsedArgs) ||
|
||||
String(
|
||||
(parsedArgs as Record<string, unknown>)[key] ?? "",
|
||||
).length === 0,
|
||||
if (missing.length === 0) {
|
||||
shouldOutputChunk = false;
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
type: "auto_approval",
|
||||
tool_name: nextName,
|
||||
tool_call_id: id,
|
||||
reason: permission.reason,
|
||||
matched_rule: permission.matchedRule,
|
||||
}),
|
||||
);
|
||||
if (missing.length === 0) {
|
||||
shouldOutputChunk = false;
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
type: "auto_approval",
|
||||
tool_name: toolCall.name,
|
||||
tool_call_id: id,
|
||||
reason: permission.reason,
|
||||
matched_rule: permission.matchedRule,
|
||||
}),
|
||||
);
|
||||
autoApprovalEmitted.add(id);
|
||||
}
|
||||
autoApprovalEmitted.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user