Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -508,11 +508,11 @@ describe("CodexAdapterV2 runtime policy", () => {

assert.equal(params.collaborationMode?.mode, "default");
assert.include(
params.additionalContext?.t3_code_orchestration?.value ?? "",
params.additionalContext?.t3_code_orchestration_01?.value ?? "",
"Use `delegate_task`",
);
assert.include(
params.additionalContext?.t3_code_orchestration?.value ?? "",
params.additionalContext?.t3_code_orchestration_02?.value ?? "",
"structured object, never as JSON text",
);
}),
Expand Down Expand Up @@ -2748,7 +2748,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
hasT3Mcp: true,
});
assert.include(
params.additionalContext?.t3_code_orchestration?.value ?? "",
params.additionalContext?.t3_code_orchestration_01?.value ?? "",
"delegate_task",
);
const entries = codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt: "work" });
Expand Down
54 changes: 54 additions & 0 deletions apps/server/src/provider/CodexDeveloperInstructions.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@
import * as NodeAssert from "node:assert/strict";

import {
T3_CODE_ORCHESTRATION_INSTRUCTIONS,
T3_CODE_ORCHESTRATION_SECTIONS,
} from "@t3tools/provider-core/server/orchestrationInstructions";
import { describe, it } from "vite-plus/test";

import {
buildCodexAdditionalContext,
buildCodexDeveloperInstructions,
CODEX_ADDITIONAL_CONTEXT_VALUE_MAX_BYTES,
} from "./CodexDeveloperInstructions.ts";

describe("buildCodexAdditionalContext", () => {
describe.each([
{ model: "gpt-5.3-codex", reasoningEffort: "high" },
{ model: "custom-model", modelName: "Custom 🦊 Model", reasoningEffort: "xhigh" },
])("with runtime $model", (runtime) => {
it.each([
undefined,
false,
true,
{ browser: false, device: false },
{ browser: true, device: false },
{ browser: false, device: true },
{ browser: true, device: true },
])("keeps every entry within Codex's per-entry cap with tools %j", (availability) => {
const context = buildCodexAdditionalContext(runtime, availability);

for (const [key, entry] of Object.entries(context)) {
// Codex counts ceil(UTF-8 bytes / 4) tokens, excluding keys and wrappers.
const bytes = Buffer.byteLength(entry.value, "utf8");
const limit = CODEX_ADDITIONAL_CONTEXT_VALUE_MAX_BYTES;
NodeAssert.ok(
bytes <= limit,
`${key}: ${bytes} UTF-8 bytes exceeds Codex's ${limit}-byte per-entry cap by ${bytes - limit} bytes; split the instructions into separate entries to avoid silent truncation`,
);
}
});
});

it("delivers all orchestration sections in authored order when keys are sorted", () => {
const context = buildCodexAdditionalContext({
model: "gpt-5.3-codex",
reasoningEffort: "high",
});
const sections = Object.keys(context)
.filter((key) => key.startsWith("t3_code_orchestration_"))
.sort()
.map((key) => context[key]!);

NodeAssert.deepEqual(
sections,
T3_CODE_ORCHESTRATION_SECTIONS.map((value) => ({ kind: "application", value })),
);
NodeAssert.equal(
sections.map((entry) => entry.value).join("\n\n"),
T3_CODE_ORCHESTRATION_INSTRUCTIONS.trim(),
);
});
});

describe("buildCodexDeveloperInstructions", () => {
it("appends runtime info after the mode instructions", () => {
const instructions = runtimeInstructions({
Expand Down
19 changes: 16 additions & 3 deletions apps/server/src/provider/CodexDeveloperInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ import { buildRuntimeInstructions } from "@t3tools/provider-core/server/runtimeI

import {
T3_CODE_BROWSER_TOOL_INSTRUCTIONS,
T3_CODE_ORCHESTRATION_INSTRUCTIONS,
T3_CODE_ORCHESTRATION_SECTIONS,
} from "@t3tools/provider-core/server/orchestrationInstructions";

/**
* Codex cuts the middle out of any `additionalContext` value over 1,000 tokens,
* which it counts as UTF-8 bytes divided by four. It does this silently, so
* every value built here has to fit.
*/
export const CODEX_ADDITIONAL_CONTEXT_VALUE_MAX_BYTES = 4_000;

const T3_CODE_DEVICE_TOOL_INSTRUCTIONS = `## T3 Code devices

The \`t3-code\` MCP server also exposes \`device_*\` tools for iOS Simulators and Android Emulators on this environment. For mobile verification, call \`device_list\`, then \`device_open\` so the user can watch the device in their Device panel; its result explains how to drive the device. Driving happens through the \`agent-device\` CLI, using the exact launcher path returned by \`device_open\`. Keep the host config and session flags returned by \`device_open\` on every command so concurrent devices stay independent: prefer \`agent-device snapshot -i\` refs over coordinates, and use \`device_screenshot\` when you need to see the screen. Prefer these tools and \`agent-device\` for opening and driving devices. Platform tools such as \`xcrun simctl\` and \`adb\` remain available for anything they do not cover, such as builds, logs, or port forwarding. If \`device_list\` reports a platform as unavailable, say so.`;
Expand Down Expand Up @@ -213,9 +220,15 @@ export function buildCodexAdditionalContext(
toolsAvailable: boolean | T3CodeToolAvailability = true,
): Record<string, V2TurnStartParams__AdditionalContextEntry> {
const tools = toolInstructions(toolsAvailable);
// Separate keys keep each value under Codex's per-entry token cap.
// Separate keys keep each value under Codex's per-entry cap. Codex emits
// entries in key order, so the numbering preserves the authored order.
return {
t3_code_orchestration: { kind: "application", value: T3_CODE_ORCHESTRATION_INSTRUCTIONS },
...Object.fromEntries(
T3_CODE_ORCHESTRATION_SECTIONS.map((section, index) => [
`t3_code_orchestration_${String(index + 1).padStart(2, "0")}`,
{ kind: "application" as const, value: section },
]),
),
t3_code_runtime: {
kind: "application",
value: buildRuntimeInstructions({ harness: "Codex", ...runtime }),
Expand Down
31 changes: 19 additions & 12 deletions packages/provider-core/src/server/orchestrationInstructions.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import type { ProviderInteractionMode } from "@t3tools/contracts";

export const T3_CODE_ORCHESTRATION_INSTRUCTIONS = `

## T3 Code orchestration
/**
* Authored as self-contained sections so a provider that limits the size of
* each instruction entry can deliver every one whole. Codex cuts the middle out
* of any entry over 4,000 bytes, so start a new section before one gets close.
*/
export const T3_CODE_ORCHESTRATION_SECTIONS: ReadonlyArray<string> = [
`## T3 Code orchestration

The \`t3-code\` MCP server provides app-owned orchestration. Treat these concepts distinctly:

- A delegated task/subagent is child work owned by the current thread. Use \`orchestrator_capabilities\` to discover the current provider/model IDs from the same live catalog as the composer, including configured custom models. Do not treat a native tool's model list as the full list of available subagent models. Prefer native subagent tools for same-provider work only when they support the chosen model. Use \`delegate_task\` with that provider instance and model when native tools cannot, including for same-provider work. Also use \`delegate_task\` for cross-provider or explicitly T3-owned child tasks. Retain each returned \`taskId\`, and use \`task_status\` or \`task_cancel\` to manage it. The returned \`childThreadId\` is backing storage for the subagent, not the target for starting another delegated review round.
- \`t3_thread_launch\` and \`create_threads\` create ordinary top-level T3 conversations. Use them only when the user explicitly asks for separate/new/top-level threads or conversations. Never use them merely because the user said "subagent" or requested parallel delegated work.
- For every T3 delegated review round, call \`delegate_task\` again. Include the original brief, prior findings, responses, and unresolved objections in each new task prompt. Track each round by its own \`taskId\`. Use a distinct \`clientRequestId\` per round, stable across retries of that round. Do not use \`t3_thread_send\` on \`childThreadId\` to continue a delegated review.
- \`schedule_task\` creates persistent recurring work in the app scheduler. Pass \`schedule\` as a structured object, never as JSON text: \`{"type":"interval","everyMs":3600000}\` for an interval, or \`{"type":"fixed_time","timeOfDay":"09:00","weekdays":[1,2,3,4,5]}\` for a wall-clock schedule, or \`{"type":"webhook"}\` to run on each request to the returned \`webhookUrl\` (the run sees the request only through \`{{body.path}}\`-style placeholders in the prompt). By default runs return to the current thread, which suits orchestrating: each trigger arrives here and you delegate or dedupe; set \`bindToCurrentThread=false\` only when the user wants a fresh thread for every run. After scheduling a timer, report the returned cadence and next run time; for a webhook, report its \`webhookUrl\`, or say T3 Connect remote access is needed if it is missing.
- For every T3 delegated review round, call \`delegate_task\` again. Include the original brief, prior findings, responses, and unresolved objections in each new task prompt. Track each round by its own \`taskId\`. Use a distinct \`clientRequestId\` per round, stable across retries of that round. Do not use \`t3_thread_send\` on \`childThreadId\` to continue a delegated review.`,
`- \`schedule_task\` creates persistent recurring work in the app scheduler. Pass \`schedule\` as a structured object, never as JSON text: \`{"type":"interval","everyMs":3600000}\` for an interval, or \`{"type":"fixed_time","timeOfDay":"09:00","weekdays":[1,2,3,4,5]}\` for a wall-clock schedule, or \`{"type":"webhook"}\` to run on each request to the returned \`webhookUrl\` (the run sees the request only through \`{{body.path}}\`-style placeholders in the prompt). By default runs return to the current thread, which suits orchestrating: each trigger arrives here and you delegate or dedupe; set \`bindToCurrentThread=false\` only when the user wants a fresh thread for every run. After scheduling a timer, report the returned cadence and next run time; for a webhook, report its \`webhookUrl\`, or say T3 Connect remote access is needed if it is missing.
- When you need a secret from the user (a token, API key, or webhook signing secret), call \`request_secret\` so they enter it privately, then pass the returned \`secretRef\` to the tool that needs it, e.g. \`signature.secretRef\` on a webhook task for a sender that signs requests such as GitHub. A \`secretRef\` works once. Never ask for a secret in chat, never invent one, and never repeat one.
- To mention another thread to the user, link it as \`[title](t3-thread://v1/<threadId>)\` with its exact \`threadId\`, not URL-encoded. T3 Code opens the thread in the app and shows its current title.

### Choose the workspace before starting a new thread
- To mention another thread to the user, link it as \`[title](t3-thread://v1/<threadId>)\` with its exact \`threadId\`, not URL-encoded. T3 Code opens the thread in the app and shows its current title.`,
`### Choose the workspace before starting a new thread

For independent implementation or a PR stack in its own worktree, use \`t3_thread_launch\` with an explicit \`workspaceStrategy\`. It creates or selects the workspace, binds the new thread to it, and prepares it before the agent starts. Put the task in \`message\`, not \`prompt\`:

Expand All @@ -25,15 +28,19 @@ For stacked work, set \`baseRef\` to the intended parent branch and \`startFromO

\`t3_thread_launch\` is the single-thread launch tool. Use \`create_threads\` only for a batch of threads intentionally sharing the caller's checkout: it always inherits the caller's project, branch, and worktree and has no workspace override. Asking an agent to run \`git worktree add\` or \`cd\` in its prompt does not update T3's thread binding. Select the workspace in the launch call instead. \`t3_worktree_handoff\` moves the calling thread, not another thread, and cannot move a thread already attached to a worktree.

\`t3_thread_launch\` has no idempotency key. Retain its returned threadId and inspect it with \`t3_thread_read\` / \`t3_thread_wait\`; preparation can still be running after acceptance. If a launch fails or its response is lost, inspect \`t3_thread_list\` before retrying, since a thread may already exist.

Tool names may include a harness-normalized MCP prefix, such as \`mcp__t3_code__delegate_task\`; the semantics are the same. Some harnesses attach optional MCP servers lazily: if an initial tool-catalog scan does not show T3 tools, do not conclude that cross-provider delegation is unavailable. Make one bounded direct attempt using the known T3 tool name on the next tool step. In Codex code mode, for example, call \`tools.mcp__t3_code__orchestrator_capabilities({})\` before reporting that the capability is absent. Keep polling/wait loops bounded, do not duplicate active work, and use stable \`clientRequestId\` values when retrying tools that accept them.
\`t3_thread_launch\` has no idempotency key. Retain its returned threadId and inspect it with \`t3_thread_read\` / \`t3_thread_wait\`; preparation can still be running after acceptance. If a launch fails or its response is lost, inspect \`t3_thread_list\` before retrying, since a thread may already exist.`,
`Tool names may include a harness-normalized MCP prefix, such as \`mcp__t3_code__delegate_task\`; the semantics are the same. Some harnesses attach optional MCP servers lazily: if an initial tool-catalog scan does not show T3 tools, do not conclude that cross-provider delegation is unavailable. Make one bounded direct attempt using the known T3 tool name on the next tool step. In Codex code mode, for example, call \`tools.mcp__t3_code__orchestrator_capabilities({})\` before reporting that the capability is absent. Keep polling/wait loops bounded, do not duplicate active work, and use stable \`clientRequestId\` values when retrying tools that accept them.

ACP fallback: some ACP agents accept the injected MCP server but fail to expose its tools. When the T3 tools are absent and \`T3_ACP_MCP_NODE\` is present, call the same tools through the terminal: \`ELECTRON_RUN_AS_NODE=1 "$T3_ACP_MCP_NODE" \${T3_ACP_MCP_ENTRYPOINT:+"$T3_ACP_MCP_ENTRYPOINT"} acp-mcp-call orchestrator_capabilities '{}'\` (\`T3_ACP_MCP_ENTRYPOINT\` is unset when T3 runs as a standalone executable). Delegate with \`acp-mcp-call delegate_task '{"task":"...","target":{"providerInstanceId":"...","model":"..."},"mode":"async","clientRequestId":"..."}'\`. This is the supported T3 transport fallback, not an ordinary shell-based substitute for delegation.

### Showing visuals

When a chart, table, diagram, image collage, or mockup would say more than prose, build a self-contained HTML page, check it with \`html_preview\`, then publish it with \`html_render\` before your final reply. The reader sees the page above that reply, so don't announce or restate it; add only what it doesn't say.
When a chart, table, diagram, image collage, or mockup would say more than prose, build a self-contained HTML page, check it with \`html_preview\`, then publish it with \`html_render\` before your final reply. The reader sees the page above that reply, so don't announce or restate it; add only what it doesn't say.`,
];

export const T3_CODE_ORCHESTRATION_INSTRUCTIONS = `

${T3_CODE_ORCHESTRATION_SECTIONS.join("\n\n")}
`;

export const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `
Expand Down
Loading