{"repo": "bytequay/bytequay", "files": {"backend/src/main/java/com/bytequay/app/service/threads/LogicLoopThreadAgent.java": {"7ca60244a527eb5aaf72396bd831098530712955": {"t": 1786067594.0540316, "n": 66868, "h": "f09441bf634e914e", "c": "/*\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.bytequay.app.service.threads;\n\nimport com.bytequay.app.domain.AgentMetrics;\nimport com.bytequay.app.domain.CredentialType;\nimport com.bytequay.app.domain.PermissionDecision;\nimport com.bytequay.app.domain.StreamEvent;\nimport com.bytequay.app.domain.Thread;\nimport com.bytequay.app.domain.ThreadKind;\nimport com.bytequay.app.domain.ThreadMessage;\nimport com.bytequay.app.domain.ThreadScope;\nimport com.bytequay.app.domain.ThreadStatus;\nimport com.bytequay.app.domain.WorkModel;\nimport com.bytequay.app.domain.WorkModelKind;\nimport com.bytequay.app.repository.ThreadStore;\nimport com.bytequay.app.service.CredentialService;\nimport com.bytequay.app.service.agents.AgentContextCompiler;\nimport com.bytequay.app.service.agents.ToolCall;\nimport com.bytequay.app.service.agents.ToolExecutor;\nimport com.bytequay.app.service.agents.TurnHooks;\nimport com.bytequay.app.service.agents.TurnResult;\nimport com.bytequay.app.service.agents.TurnRunner;\nimport com.bytequay.app.service.agents.TurnSpec;\nimport com.bytequay.app.service.ai.ModelPricing;\nimport com.bytequay.app.service.local.ds4.Ds4Instrumentation;\nimport com.bytequay.app.service.local.ds4.Ds4LifecycleService;\nimport com.bytequay.app.service.local.ds4.Ds4State;\nimport com.bytequay.app.service.skills.ManagedSkill;\nimport com.bytequay.app.service.skills.ManagedSkillBundle;\nimport com.bytequay.app.service.threads.tools.AgentTool;\nimport com.bytequay.app.service.threads.tools.AgentToolContext;\nimport com.bytequay.app.service.threads.tools.LogicLoopToolRegistry;\nimport com.bytequay.app.service.threads.tools.ToolPermissionMediator;\nimport com.bytequay.app.service.tools.AgentRole;\nimport com.bytequay.app.service.tools.Gating;\nimport com.bytequay.app.service.workspaces.WorkspaceDocumentLoader;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.fasterxml.jackson.databind.node.ObjectNode;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.io.IOException;\nimport java.net.http.HttpClient;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.Base64;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.OptionalInt;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionStage;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\n\nimport static java.util.Objects.requireNonNull;\n\n/**\n * In-JVM API-lane {@link ThreadAgent}. Drives the loop by calling the\n * resolved provider's HTTP API directly and translating streaming deltas\n * into the same {@link StreamEvent} shapes the CLI lane emits.\n *\n *
B5 multi-provider transport: Anthropic (SSE Messages API), OpenAI\n * (chat-completions SSE), and DeepSeek (OpenAI-compatible surface, cloud\n * and local ds4 variant) are all wired. Tools route through\n * {@link LogicLoopToolRegistry} on all three providers; the Anthropic path\n * uses {@code tool_use} content blocks while OpenAI/DeepSeek use the\n * {@code tool_calls} message role.\n *\n *
History replay limits to {@value REPLAY_HISTORY_LIMIT} recent text\n * messages per turn. Tool-call / tool-result rows from prior turns are\n * excluded — only the final assistant text survives across turn boundaries.\n *\n *
Mirrors {@link ClaudeCodeCliThreadAgent} on the contract that matters\n * for callers: same {@link ThreadStore#appendMessage} writes, same event\n * ordering (SessionStarted → AssistantTextDelta… → AssistantText →\n * UsageUpdated → TurnDone → SessionEnded), same lifecycle state machine\n * (IDLE/RUNNING/AWAITING/STOPPED), so the frontend renders trunk and task\n * panes identically across lanes.\n */\npublic class LogicLoopThreadAgent\n implements ThreadAgent\n{\n private static final Logger log = LoggerFactory.getLogger(LogicLoopThreadAgent.class);\n\n // ── Anthropic ─────────────────────────────────────────────────────────\n private static final String ANTHROPIC_PROVIDER_ID = \"anthropic\";\n private static final String ANTHROPIC_MESSAGES_URL = \"https://api.anthropic.com/v1/messages\";\n\n // ── OpenAI ────────────────────────────────────────────────────────────\n private static final String OPENAI_PROVIDER_ID = \"openai\";\n private static final String OPENAI_COMPLETIONS_URL = \"https://api.openai.com/v1/chat/completions\";\n\n // ── DeepSeek ──────────────────────────────────────────────────────────\n private static final String DEEPSEEK_PROVIDER_ID = \"deepseek\";\n private static final String DEEPSEEK_COMPLETIONS_URL = \"https://api.deepseek.com/chat/completions\";\n /** Local ds4 model served by the Ds4LifecycleService subprocess. */\n /** Fallback local URL when no Ds4LifecycleService is wired (test\n * paths). Production resolves the URL dynamically from the\n * supervisor's status endpoint so a user-configured ds4.port\n * flows through end-to-end.\n *
Note the {@code /v1/} prefix — ds4-server's README pins\n * the chat-completions endpoint at {@code POST /v1/chat/completions};\n * hitting {@code /chat/completions} (without {@code /v1/}) was\n * the bug that surfaced as the trunk thread flipping to ERRORED. */\n private static final String DEEPSEEK_LOCAL_COMPLETIONS_URL = \"http://127.0.0.1:8000/v1/chat/completions\";\n private static final String DEEPSEEK_LOCAL_MODEL_ID = \"deepseek-v4-flash\";\n /** Placeholder token the local ds4 server accepts; the real gate is\n * \"server running\", not \"key present\". */\n private static final String DEEPSEEK_LOCAL_AUTH_TOKEN = \"dsv4-local\";\n\n private static final int MAX_OUTPUT_TOKENS = 4_096;\n /** History tail replayed to the provider on each turn. Bound by row\n * count rather than token budget for simplicity in v1. */\n private static final int REPLAY_HISTORY_LIMIT = 80;\n /** Safety cap on the per-turn tool-use ↔ result loop. Reset on\n * every {@code send()}. */\n private static final int MAX_TOOL_ITERATIONS = 12;\n\n // ── Fields ────────────────────────────────────────────────────────────\n\n private final String threadId;\n /** Task attribution for the current scheduler turn. A BRAIN_AGENT starts\n * with its 1:1 parent task; stage agents receive the explicit turn task\n * immediately before dispatch. */\n private volatile String activeTaskId;\n private final ThreadKind kind;\n private final ThreadStore store;\n /** Stage of the in-flight turn, set by the scheduler before each send;\n * emitted messages inherit it as their explicit stage_id. */\n private volatile String activeStageId;\n /** AgentRun episode of the in-flight turn, when this turn belongs to one. */\n private volatile String activeAgentRunId;\n private volatile ManagedSkillBundle managedSkillBundle = ManagedSkillBundle.empty();\n private volatile List Tools that intentionally stay off the trunk:\n * every {@code PublishToolHandlers} mutator (push, merge_pr,\n * approve_pr, open_pr, post_comment, …), {@code run_shell},\n * {@code run_checks}, and provider-driven skill discovery. Add a name here only\n * after confirming the trunk can complete its planning loop without\n * it. */\n // Package-private so the characterization test can pin the contract:\n // the trunk must be able to plan and queue, not just cut one task.\n static final Set \"No active task on the thread\" is a good-enough proxy for\n * \"this is a trunk turn\" because the trunk agent only runs when\n * the user typed into the trunk composer (which doesn't open a\n * task), and the task agent only runs while a task is active.\n * If we later allow trunk turns while a task is also active\n * (e.g. background planning while a task executes), we'll plumb\n * an explicit {@code isTrunk} flag through the constructor and\n * switch this to read it. */\n private boolean isTrunkTurn()\n {\n return activeTaskId() == null;\n }\n\n private String encodeText(String text)\n {\n try {\n ObjectNode node = mapper.createObjectNode();\n node.put(\"text\", text == null ? \"\" : text);\n return mapper.writeValueAsString(node);\n }\n catch (Exception e) {\n throw new IllegalStateException(\"Failed to encode message text\", e);\n }\n }\n\n // ── Event publishing ──────────────────────────────────────────────────\n\n private void publish(StreamEvent event)\n {\n for (Consumer