fix: Patch headless mode GPT-5 (#88)

Co-authored-by: cpacker <packercharles@gmail.com>
This commit is contained in:
Kevin Lin
2025-11-16 20:48:50 -08:00
committed by GitHub
parent 8df4c08510
commit 754db3b273
8 changed files with 75 additions and 46 deletions

View File

@@ -6,6 +6,7 @@ import type {
} from "@letta-ai/letta-client/resources/agents/agents";
import type { ApprovalCreate } from "@letta-ai/letta-client/resources/agents/messages";
import type { StopReasonType } from "@letta-ai/letta-client/resources/runs/runs";
import type { ApprovalResult } from "./agent/approval-execution";
import { getClient } from "./agent/client";
import { createAgent } from "./agent/create";
import { sendMessageStream } from "./agent/message";
@@ -15,7 +16,7 @@ import { createBuffers, toLines } from "./cli/helpers/accumulator";
import { safeJsonParseOr } from "./cli/helpers/safeJsonParse";
import { drainStreamWithResume } from "./cli/helpers/stream";
import { settingsManager } from "./settings-manager";
import { checkToolPermission, executeTool } from "./tools/manager";
import { checkToolPermission } from "./tools/manager";
export async function handleHeadlessCommand(
argv: string[],
@@ -213,7 +214,7 @@ export async function handleHeadlessCommand(
const decisions: Decision[] = [];
for (const currentApproval of pendingApprovals) {
const { toolCallId, toolName, toolArgs } = currentApproval;
const { toolName, toolArgs } = currentApproval;
const parsedArgs = safeJsonParseOr<Record<string, unknown>>(
toolArgs || "{}",
{},
@@ -239,8 +240,7 @@ export async function handleHeadlessCommand(
const required =
(schema?.input_schema?.required as string[] | undefined) || [];
const missing = required.filter(
(key) =>
!(key in parsedArgs) || String(parsedArgs[key] ?? "").length === 0,
(key) => !(key in parsedArgs) || parsedArgs[key] == null,
);
if (missing.length > 0) {
decisions.push({
@@ -283,7 +283,7 @@ export async function handleHeadlessCommand(
// Send all results in one batch
const approvalInput: ApprovalCreate = {
type: "approval",
approvals: executedResults as any,
approvals: executedResults as ApprovalResult[],
};
// Send the approval to clear the pending state; drain the stream without output
@@ -428,8 +428,19 @@ export async function handleHeadlessCommand(
: [];
for (const toolCall of toolCalls) {
const id = toolCall?.tool_call_id;
if (!id) continue; // remain strict: do not invent ids
// Many backends stream tool_call chunks where only the first frame
// carries the tool_call_id; subsequent argument deltas omit it.
// Fall back to the last seen id within this turn so we can
// properly accumulate args.
let id: string | null = toolCall?.tool_call_id ?? _lastApprovalId;
if (!id) {
// As an additional guard, if exactly one approval is being
// tracked already, use that id for continued argument deltas.
if (approvalRequests.size === 1) {
id = Array.from(approvalRequests.keys())[0] ?? null;
}
}
if (!id) continue; // cannot safely attribute this chunk
_lastApprovalId = id;
@@ -437,9 +448,7 @@ export async function handleHeadlessCommand(
const prev = approvalRequests.get(id);
const base = prev?.args ?? "";
const incomingArgs =
toolCall?.arguments && toolCall.arguments.trim().length > 0
? base + toolCall.arguments
: base;
toolCall?.arguments != null ? base + toolCall.arguments : base;
// Preserve previously seen name; set if provided in this chunk
const nextName = toolCall?.name || prev?.toolName || "";
@@ -484,8 +493,7 @@ export async function handleHeadlessCommand(
const missing = required.filter(
(key) =>
!(key in parsedArgs) ||
String((parsedArgs as Record<string, unknown>)[key] ?? "")
.length === 0,
(parsedArgs as Record<string, unknown>)[key] == null,
);
if (missing.length === 0) {
shouldOutputChunk = false;
@@ -586,7 +594,7 @@ export async function handleHeadlessCommand(
const decisions: Decision[] = [];
for (const currentApproval of approvals) {
const { toolCallId, toolName, toolArgs } = currentApproval;
const { toolName, toolArgs } = currentApproval;
// Check permission using existing permission system
const parsedArgs = safeJsonParseOr<Record<string, unknown>>(
@@ -622,9 +630,7 @@ export async function handleHeadlessCommand(
const required =
(schema?.input_schema?.required as string[] | undefined) || [];
const missing = required.filter(
(key) =>
!(key in parsedArgs) ||
String(parsedArgs[key] ?? "").length === 0,
(key) => !(key in parsedArgs) || parsedArgs[key] == null,
);
if (missing.length > 0) {
// Auto-deny with a clear reason so the model can retry with arguments
@@ -653,7 +659,7 @@ export async function handleHeadlessCommand(
currentInput = [
{
type: "approval",
approvals: executedResults as any,
approvals: executedResults as ApprovalResult[],
},
];
continue;