源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
package agentruntime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
||||
)
|
||||
|
||||
func (a *CodexAppServerAdapter) appServerServerRequest(
|
||||
ctx context.Context,
|
||||
client *codexAppServerClient,
|
||||
session Session,
|
||||
turnID string,
|
||||
message acpMessage,
|
||||
normalizer *acpTurnNormalizer,
|
||||
emit EventSink,
|
||||
) ([]activityshared.Event, error) {
|
||||
if strings.TrimSpace(turnID) == "" || emit == nil {
|
||||
err := errors.New("approval request outside active prompt turn is not supported")
|
||||
_ = client.Respond(ctx, message.ID, nil, &acpError{Code: -32000, Message: err.Error()})
|
||||
return nil, err
|
||||
}
|
||||
params := map[string]any{}
|
||||
if len(message.Params) > 0 {
|
||||
if err := json.Unmarshal(message.Params, ¶ms); err != nil {
|
||||
_ = client.Respond(ctx, message.ID, nil, &acpError{Code: -32602, Message: err.Error()})
|
||||
return nil, fmt.Errorf("invalid approval request: %w", err)
|
||||
}
|
||||
}
|
||||
eventSession, eventTurnID, child, err := a.appServerInteractiveRequestScope(session, turnID, params)
|
||||
if err != nil {
|
||||
_ = client.Respond(ctx, message.ID, nil, &acpError{Code: -32000, Message: err.Error()})
|
||||
return nil, err
|
||||
}
|
||||
eventNormalizer := normalizer
|
||||
if child != nil {
|
||||
eventNormalizer = child.normalizer
|
||||
}
|
||||
events, pending, err := a.appServerApprovalRequested(eventSession, eventTurnID, message.ID, message.Method, params, eventNormalizer)
|
||||
if err != nil {
|
||||
_ = client.Respond(ctx, message.ID, nil, &acpError{Code: -32602, Message: err.Error()})
|
||||
return events, err
|
||||
}
|
||||
events = appServerEventsForChild(events, child)
|
||||
if len(events) > 0 {
|
||||
emit(events)
|
||||
}
|
||||
processingTurn := a.activeTurnForNormalizer(session.AgentSessionID, normalizer)
|
||||
go a.respondAppServerServerRequest(ctx, client, eventSession, eventTurnID, child, message, params, pending, processingTurn, emit)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *CodexAppServerAdapter) appServerInteractiveRequestScope(
|
||||
root Session,
|
||||
rootTurnID string,
|
||||
params map[string]any,
|
||||
) (Session, string, *codexAppServerThreadContext, error) {
|
||||
rootProviderThreadID := strings.TrimSpace(root.ProviderSessionID)
|
||||
requestThreadID := appServerMessageThreadID(params)
|
||||
if requestThreadID == "" || rootProviderThreadID == "" || requestThreadID == rootProviderThreadID {
|
||||
return root, rootTurnID, nil, nil
|
||||
}
|
||||
child, ok := a.appServerChildThread(root.AgentSessionID, requestThreadID)
|
||||
if !ok {
|
||||
return Session{}, "", nil, fmt.Errorf("interactive request references unknown child thread %q", requestThreadID)
|
||||
}
|
||||
return appServerChildSession(root, requestThreadID, child), child.turnID, child, nil
|
||||
}
|
||||
|
||||
func (a *CodexAppServerAdapter) respondAppServerServerRequest(
|
||||
ctx context.Context,
|
||||
client *codexAppServerClient,
|
||||
session Session,
|
||||
turnID string,
|
||||
child *codexAppServerThreadContext,
|
||||
message acpMessage,
|
||||
params map[string]any,
|
||||
pending *pendingInteractiveRequest,
|
||||
processingTurn *codexAppServerActiveTurn,
|
||||
emit EventSink,
|
||||
) {
|
||||
if pending == nil {
|
||||
return
|
||||
}
|
||||
selection, err := pending.wait(ctx)
|
||||
if err != nil {
|
||||
pending.finish(pendingInteractiveRequestStateInterrupted)
|
||||
resolved := normalizedPermissionResolvedEvents(session, turnID, pending, pendingInteractiveResponse{}, err)
|
||||
// The shared error path emits only call.failed; append the
|
||||
// back-to-running turn.updated so the lifecycle cannot strand in
|
||||
// waiting_approval when a request is rejected or canceled.
|
||||
resolved = append(resolved, newTurnActivityEvent(session, EventTurnUpdated, turnID, SessionStatusWorking, "", "", map[string]any{
|
||||
"phase": string(activityshared.TurnPhaseWorking),
|
||||
"requestId": pending.requestID,
|
||||
}))
|
||||
resolved = appServerEventsForChild(resolved, child)
|
||||
if emit != nil {
|
||||
emit(resolved)
|
||||
}
|
||||
_ = client.Respond(ctx, message.ID, nil, &acpError{Code: -32000, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
if selection.outOfBandResolved {
|
||||
resolved := appServerEventsForChild(acpPermissionOutOfBandResolvedEvents(session, turnID, pending), child)
|
||||
if emit != nil {
|
||||
emit(resolved)
|
||||
}
|
||||
return
|
||||
}
|
||||
// A provider may complete the turn immediately after accepting this
|
||||
// response. Serialize the acknowledgement and its resolution events with
|
||||
// turn finalization so call.completed cannot arrive after the terminal event
|
||||
// and get dropped by the turn's closed-event guard.
|
||||
if processingTurn != nil {
|
||||
processingTurn.processMu.Lock()
|
||||
defer processingTurn.processMu.Unlock()
|
||||
}
|
||||
result, responseErr := appServerApprovalResult(message.Method, params, selection)
|
||||
if err := client.Respond(ctx, message.ID, result, responseErr); err != nil {
|
||||
if pending.finish(pendingInteractiveRequestStateSuperseded) && emit != nil {
|
||||
emit(appServerEventsForChild(normalizedPermissionResolvedEvents(session, turnID, pending, selection, err), child))
|
||||
}
|
||||
return
|
||||
}
|
||||
if pending.finish(pendingInteractiveRequestStateAnswered) {
|
||||
if emit != nil {
|
||||
emit(appServerEventsForChild(normalizedPermissionResolvedEvents(session, turnID, pending, selection, nil), child))
|
||||
}
|
||||
a.scheduleAppServerInteractiveDenyFeedback(ctx, client, session, pending, selection)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CodexAppServerAdapter) scheduleAppServerInteractiveDenyFeedback(
|
||||
ctx context.Context,
|
||||
client *codexAppServerClient,
|
||||
session Session,
|
||||
pending *pendingInteractiveRequest,
|
||||
selection pendingInteractiveResponse,
|
||||
) {
|
||||
feedback := strings.TrimSpace(asString(selection.payload["denyMessage"]))
|
||||
if feedback == "" || !isDenyInteractiveSelectionValue(selection.optionID) {
|
||||
return
|
||||
}
|
||||
threadID := strings.TrimSpace(session.ProviderSessionID)
|
||||
providerTurnID := strings.TrimSpace(pending.providerTurnID)
|
||||
if threadID == "" || providerTurnID == "" {
|
||||
slog.Warn("agent interactive deny feedback cannot be steered without provider identity",
|
||||
"agent_session_id", session.AgentSessionID,
|
||||
"turn_id", pending.turnID,
|
||||
"request_id", pending.requestID,
|
||||
)
|
||||
return
|
||||
}
|
||||
timeout := a.turnSteerTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultCodexAppServerTurnSteerTimeout
|
||||
}
|
||||
go func() {
|
||||
_, err := client.TurnSteerNoHandler(context.WithoutCancel(ctx), timeout, map[string]any{
|
||||
"threadId": threadID,
|
||||
"expectedTurnId": providerTurnID,
|
||||
"input": appServerUserInput([]PromptContentBlock{{
|
||||
Type: "text", Text: feedback,
|
||||
}}),
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("agent interactive deny feedback steer failed",
|
||||
"agent_session_id", session.AgentSessionID,
|
||||
"provider_thread_id", threadID,
|
||||
"provider_turn_id", providerTurnID,
|
||||
"request_id", pending.requestID,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *CodexAppServerAdapter) appServerApprovalRequested(
|
||||
session Session,
|
||||
turnID string,
|
||||
rawRequestID json.RawMessage,
|
||||
method string,
|
||||
params map[string]any,
|
||||
normalizer *acpTurnNormalizer,
|
||||
) ([]activityshared.Event, *pendingInteractiveRequest, error) {
|
||||
requestID := acpRequestID(rawRequestID)
|
||||
if requestID == "" {
|
||||
return nil, nil, errors.New("approval request id is required")
|
||||
}
|
||||
if method == appServerMethodRequestUserInput {
|
||||
return a.appServerUserInputRequested(session, turnID, requestID, params)
|
||||
}
|
||||
if method == appServerMethodMCPElicitation {
|
||||
return a.appServerMCPElicitationRequested(session, turnID, requestID, params)
|
||||
}
|
||||
toolCall := appServerApprovalToolCall(method, params)
|
||||
options := appServerApprovalOptions(method)
|
||||
title := firstNonEmpty(asString(toolCall["title"]), "Permission requested")
|
||||
callID := firstNonEmpty(asString(toolCall["toolCallId"]), newID())
|
||||
status := string(activityshared.TurnPhaseWaitingApproval)
|
||||
approvalPurpose := normalizedApprovalPurpose(toolCall)
|
||||
knownInput := normalizer.KnownToolCallInput(asString(toolCall["toolCallId"]))
|
||||
input := normalizedApprovalInput(toolCall, options, requestID, knownInput)
|
||||
payload := map[string]any{
|
||||
"callId": callID,
|
||||
"callType": "approval",
|
||||
"name": title,
|
||||
"toolName": "Approval",
|
||||
"status": status,
|
||||
"input": input,
|
||||
}
|
||||
if approvalPurpose != "" {
|
||||
payload["approvalPurpose"] = approvalPurpose
|
||||
}
|
||||
pending := &pendingInteractiveRequest{
|
||||
agentSessionID: strings.TrimSpace(session.AgentSessionID),
|
||||
turnID: strings.TrimSpace(turnID),
|
||||
requestID: requestID,
|
||||
eventID: newID(),
|
||||
callID: callID,
|
||||
callType: "approval",
|
||||
input: input,
|
||||
kind: "approval",
|
||||
providerTurnID: strings.TrimSpace(asString(params["turnId"])),
|
||||
approvalPurpose: approvalPurpose,
|
||||
name: title,
|
||||
toolName: "Approval",
|
||||
options: options,
|
||||
response: make(chan pendingInteractiveResponse, 1),
|
||||
}
|
||||
a.storePendingRequest(pending)
|
||||
return []activityshared.Event{
|
||||
newTurnActivityEvent(session, EventTurnUpdated, turnID, SessionStatusWaiting, "", "", map[string]any{
|
||||
"phase": string(activityshared.TurnPhaseWaitingApproval),
|
||||
"requestId": requestID,
|
||||
}),
|
||||
newTurnActivityEventWithID(
|
||||
session,
|
||||
pending.eventID,
|
||||
EventCallStarted,
|
||||
turnID,
|
||||
SessionStatusWaiting,
|
||||
"",
|
||||
title,
|
||||
payload,
|
||||
),
|
||||
normalizedInteractionRequestedEvent(session, turnID, pending),
|
||||
}, pending, nil
|
||||
}
|
||||
|
||||
func (*CodexAppServerAdapter) ControllerSendsInteractiveDenyFollowUp() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func appServerUnsupportedServerRequestEvents(
|
||||
session Session,
|
||||
turnID string,
|
||||
message acpMessage,
|
||||
err error,
|
||||
) []activityshared.Event {
|
||||
if strings.TrimSpace(turnID) == "" || err == nil {
|
||||
return nil
|
||||
}
|
||||
requestID := acpRequestID(message.ID)
|
||||
callID := firstNonEmpty(requestID, newID())
|
||||
return []activityshared.Event{
|
||||
newTurnActivityEventWithID(
|
||||
session,
|
||||
"server-request:"+callID,
|
||||
EventCallFailed,
|
||||
turnID,
|
||||
messageStreamStateFailed,
|
||||
"",
|
||||
"Unsupported server request",
|
||||
map[string]any{
|
||||
"callId": callID,
|
||||
"callType": "server_request",
|
||||
"name": "Unsupported server request",
|
||||
"toolName": "ServerRequest",
|
||||
"status": messageStreamStateFailed,
|
||||
"error": map[string]any{
|
||||
"requestId": requestID,
|
||||
"method": message.Method,
|
||||
"message": err.Error(),
|
||||
},
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *CodexAppServerAdapter) appServerUserInputRequested(
|
||||
session Session,
|
||||
turnID string,
|
||||
requestID string,
|
||||
params map[string]any,
|
||||
) ([]activityshared.Event, *pendingInteractiveRequest, error) {
|
||||
questions, _ := params["questions"].([]any)
|
||||
input := map[string]any{
|
||||
"requestId": requestID,
|
||||
"questions": clonePayloadValue(questions),
|
||||
}
|
||||
prompt := &SessionInteractivePrompt{
|
||||
Kind: "ask-user",
|
||||
RequestID: requestID,
|
||||
ToolName: "AskUserQuestion",
|
||||
Status: "waiting_input",
|
||||
Input: clonePayload(input),
|
||||
Metadata: map[string]any{
|
||||
"callType": "interactive",
|
||||
"interactiveKind": "ask-user",
|
||||
"toolName": "AskUserQuestion",
|
||||
},
|
||||
}
|
||||
callID := firstNonEmpty(asString(params["itemId"]), newID())
|
||||
payload := map[string]any{
|
||||
"callId": callID,
|
||||
"callType": "interactive",
|
||||
"name": "AskUserQuestion",
|
||||
"toolName": "AskUserQuestion",
|
||||
"status": "waiting_input",
|
||||
"input": clonePayload(input),
|
||||
"metadata": clonePayload(prompt.Metadata),
|
||||
}
|
||||
pending := &pendingInteractiveRequest{
|
||||
agentSessionID: strings.TrimSpace(session.AgentSessionID),
|
||||
turnID: strings.TrimSpace(turnID),
|
||||
requestID: requestID,
|
||||
eventID: newID(),
|
||||
callID: callID,
|
||||
callType: "interactive",
|
||||
input: input,
|
||||
kind: "ask-user",
|
||||
name: "AskUserQuestion",
|
||||
toolName: "AskUserQuestion",
|
||||
prompt: prompt,
|
||||
response: make(chan pendingInteractiveResponse, 1),
|
||||
}
|
||||
a.storePendingRequest(pending)
|
||||
return []activityshared.Event{
|
||||
newTurnActivityEvent(session, EventTurnUpdated, turnID, SessionStatusWaiting, "", "", map[string]any{
|
||||
"phase": string(activityshared.TurnPhaseWaitingApproval),
|
||||
"requestId": requestID,
|
||||
}),
|
||||
newTurnActivityEventWithID(
|
||||
session,
|
||||
pending.eventID,
|
||||
EventCallStarted,
|
||||
turnID,
|
||||
SessionStatusWaiting,
|
||||
"",
|
||||
"AskUserQuestion",
|
||||
payload,
|
||||
),
|
||||
normalizedInteractionRequestedEvent(session, turnID, pending),
|
||||
}, pending, nil
|
||||
}
|
||||
|
||||
func appServerApprovalToolCall(method string, params map[string]any) map[string]any {
|
||||
switch method {
|
||||
case appServerMethodCommandApproval:
|
||||
command := asStringRaw(params["command"])
|
||||
input := map[string]any{
|
||||
"command": command,
|
||||
"cwd": asString(params["cwd"]),
|
||||
}
|
||||
if reason := asStringRaw(params["reason"]); reason != "" {
|
||||
input["reason"] = reason
|
||||
}
|
||||
return map[string]any{
|
||||
"toolCallId": firstNonEmpty(asString(params["itemId"]), asString(params["approvalId"])),
|
||||
"title": firstNonEmpty(command, "Run command"),
|
||||
"kind": "execute",
|
||||
"input": input,
|
||||
}
|
||||
case appServerMethodExecApprovalV1:
|
||||
command := normalizedApprovalDisplayCommand(params["command"])
|
||||
input := map[string]any{
|
||||
"command": command,
|
||||
"cwd": asString(params["cwd"]),
|
||||
}
|
||||
if reason := asStringRaw(params["reason"]); reason != "" {
|
||||
input["reason"] = reason
|
||||
}
|
||||
return map[string]any{
|
||||
"toolCallId": firstNonEmpty(asString(params["callId"]), asString(params["approvalId"])),
|
||||
"title": firstNonEmpty(command, "Run command"),
|
||||
"kind": "execute",
|
||||
"input": input,
|
||||
}
|
||||
case appServerMethodFileChangeApproval, appServerMethodPatchApprovalV1:
|
||||
input := map[string]any{}
|
||||
if reason := asStringRaw(params["reason"]); reason != "" {
|
||||
input["reason"] = reason
|
||||
}
|
||||
if grantRoot := asString(params["grantRoot"]); grantRoot != "" {
|
||||
input["grantRoot"] = grantRoot
|
||||
}
|
||||
if fileChanges := params["fileChanges"]; fileChanges != nil {
|
||||
input["fileChanges"] = clonePayloadValue(fileChanges)
|
||||
}
|
||||
return map[string]any{
|
||||
"toolCallId": firstNonEmpty(asString(params["itemId"]), asString(params["callId"])),
|
||||
"title": "Apply file changes",
|
||||
"kind": "edit",
|
||||
"input": input,
|
||||
}
|
||||
case appServerMethodPermissionsApproval:
|
||||
input := map[string]any{
|
||||
"permissions": clonePayloadValue(params["permissions"]),
|
||||
"cwd": asString(params["cwd"]),
|
||||
}
|
||||
if reason := asStringRaw(params["reason"]); reason != "" {
|
||||
input["reason"] = reason
|
||||
}
|
||||
return map[string]any{
|
||||
"toolCallId": firstNonEmpty(asString(params["itemId"]), newID()),
|
||||
"title": "Grant additional permissions",
|
||||
"kind": "other",
|
||||
"input": input,
|
||||
}
|
||||
default:
|
||||
return map[string]any{
|
||||
"toolCallId": newID(),
|
||||
"title": "Permission requested",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appServerApprovalOptions(method string) []map[string]any {
|
||||
switch method {
|
||||
case appServerMethodPermissionsApproval:
|
||||
return []map[string]any{
|
||||
{"optionId": "approve", "name": "Approve", "kind": "allow_once"},
|
||||
{"optionId": "deny", "name": "Deny", "kind": "reject_once"},
|
||||
}
|
||||
default:
|
||||
return []map[string]any{
|
||||
{"optionId": "approve", "name": "Approve", "kind": "allow_once"},
|
||||
{"optionId": "approve_for_session", "name": "Approve for session", "kind": "allow_always"},
|
||||
{"optionId": "deny", "name": "Deny", "kind": "reject_once"},
|
||||
{"optionId": "abort", "name": "Deny and stop the turn", "kind": "reject_always"},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appServerApprovalResult(method string, params map[string]any, selection pendingInteractiveResponse) (any, *acpError) {
|
||||
optionID := strings.TrimSpace(selection.optionID)
|
||||
switch method {
|
||||
case appServerMethodCommandApproval, appServerMethodFileChangeApproval:
|
||||
decision := map[string]string{
|
||||
"approve": "accept",
|
||||
"approve_for_session": "acceptForSession",
|
||||
"deny": "decline",
|
||||
"abort": "cancel",
|
||||
}[optionID]
|
||||
if decision == "" {
|
||||
decision = "decline"
|
||||
}
|
||||
return map[string]any{"decision": decision}, nil
|
||||
case appServerMethodExecApprovalV1, appServerMethodPatchApprovalV1:
|
||||
decision := map[string]string{
|
||||
"approve": "approved",
|
||||
"approve_for_session": "approved_for_session",
|
||||
"deny": "denied",
|
||||
"abort": "abort",
|
||||
}[optionID]
|
||||
if decision == "" {
|
||||
decision = "denied"
|
||||
}
|
||||
return map[string]any{"decision": decision}, nil
|
||||
case appServerMethodPermissionsApproval:
|
||||
if optionID == "approve" {
|
||||
return map[string]any{
|
||||
"permissions": clonePayloadValue(params["permissions"]),
|
||||
"scope": "session",
|
||||
}, nil
|
||||
}
|
||||
return nil, &acpError{Code: -32000, Message: "user denied the permission request"}
|
||||
case appServerMethodRequestUserInput:
|
||||
return map[string]any{
|
||||
"answers": appServerUserInputAnswers(params, selection),
|
||||
}, nil
|
||||
case appServerMethodMCPElicitation:
|
||||
return appServerMCPElicitationResult(selection), nil
|
||||
default:
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func appServerUserInputAnswers(params map[string]any, selection pendingInteractiveResponse) map[string]any {
|
||||
answers := map[string]any{}
|
||||
// The GUI sends per-question answers keyed by question id under
|
||||
// answersByQuestionId (its `answers` field is a flat display list, not a
|
||||
// map). Accept a bare `answers` map too for callers that inline it.
|
||||
keyed := payloadObject(selection.payload["answersByQuestionId"])
|
||||
if len(keyed) == 0 {
|
||||
keyed = payloadObject(selection.payload["answers"])
|
||||
}
|
||||
if len(keyed) > 0 {
|
||||
for questionID, value := range keyed {
|
||||
answers[questionID] = map[string]any{"answers": appServerAnswerValues(value)}
|
||||
}
|
||||
return answers
|
||||
}
|
||||
questions, _ := params["questions"].([]any)
|
||||
answerText := firstNonEmpty(
|
||||
asString(selection.payload["answer"]),
|
||||
strings.TrimSpace(selection.optionID),
|
||||
)
|
||||
for _, question := range questions {
|
||||
questionID := asString(payloadObject(question)["id"])
|
||||
if questionID == "" {
|
||||
continue
|
||||
}
|
||||
answers[questionID] = map[string]any{"answers": appServerAnswerValues(answerText)}
|
||||
}
|
||||
return answers
|
||||
}
|
||||
|
||||
func appServerAnswerValues(value any) []string {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if trimmed := strings.TrimSpace(typed); trimmed != "" {
|
||||
return []string{trimmed}
|
||||
}
|
||||
return []string{}
|
||||
case []any:
|
||||
out := make([]string, 0, len(typed))
|
||||
for _, entry := range typed {
|
||||
if text := strings.TrimSpace(asString(entry)); text != "" {
|
||||
out = append(out, text)
|
||||
}
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// --- request parameter builders ---
|
||||
|
||||
// appServerThreadReasoningSummaryConfig selects the thread-level reasoning
|
||||
// summary mode for codex app-server. Inline /review turns interleave readable
|
||||
// reasoning via summaryTextDelta; the ACP transport disables summaries for
|
||||
// spark, but app-server review still needs them.
|
||||
Reference in New Issue
Block a user