Skip to content
Open
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
25 changes: 1 addition & 24 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ import {
ThreadForkServiceV2,
} from "./ThreadForkService.ts";
import { planThreadDeletion } from "./ThreadDeletion.ts";
import { delegatedCompletionWakeDetail } from "./delegatedCompletionWake.ts";
import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import * as ServerSettings from "../serverSettings.ts";

Expand Down Expand Up @@ -836,30 +837,6 @@ function legacyPullRequestLink(
);
}

const DELEGATED_RESULT_INLINE_BYTES = 16_000;

function delegatedCompletionWakeDetail(
taskIds: ReadonlyArray<string>,
tasks: ReadonlyArray<OrchestrationV2Subagent>,
): string {
let remainingBytes = DELEGATED_RESULT_INLINE_BYTES;
return taskIds
.map((taskId) => {
const task = tasks.find((candidate) => candidate.id === taskId);
const label = `Delegated task ${taskId}${task?.title == null ? "" : ` (${task.title})`}${task === undefined ? " reached a terminal state" : ` [${task.status}]`}`;
if (task?.result != null) {
const detail = `${label}:\n${task.result}`;
const bytes = Buffer.byteLength(detail);
if (bytes <= remainingBytes) {
remainingBytes -= bytes;
return detail;
}
}
return `${label}. Use task_status with taskId ${taskId} to read the result.`;
})
.join("\n\n");
}

function isTerminalDelegatedTaskStatus(status: OrchestrationV2Subagent["status"]): boolean {
return (
status === "completed" ||
Expand Down
29 changes: 29 additions & 0 deletions apps/server/src/orchestration-v2/delegatedCompletionWake.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { OrchestrationV2Subagent } from "@t3tools/contracts";

const DELEGATED_RESULT_INLINE_BYTES = 16_000;

type WakeTask = Pick<OrchestrationV2Subagent, "title" | "status" | "result"> & {
readonly id: string;
};

export function delegatedCompletionWakeDetail(
taskIds: ReadonlyArray<string>,
tasks: ReadonlyArray<WakeTask>,
): string {
let remainingBytes = DELEGATED_RESULT_INLINE_BYTES;
return taskIds
.map((taskId) => {
const task = tasks.find((candidate) => candidate.id === taskId);
const label = `Delegated task ${taskId}${task?.title == null ? "" : ` (${task.title})`}${task === undefined ? " reached a terminal state" : ` [${task.status}]`}`;
if (task?.result != null) {
const detail = `${label}:\n${task.result}`;
const bytes = Buffer.byteLength(detail);
if (bytes <= remainingBytes) {
remainingBytes -= bytes;
return detail;
}
}
return `${label}. Use task_status with taskId ${taskId} to read the result.`;
})
.join("\n\n");
}
4 changes: 2 additions & 2 deletions apps/server/src/provider/CodexDeveloperInstructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ describe("Codex orchestration context", () => {
orchestration.includes("Background agent: `delegate_task`, never a background `Agent`."),
);
NodeAssert.ok(
orchestration.includes("Every new delegate review or re-check is a new `delegate_task`"),
orchestration.includes("Each new delegate review or re-check: a new `delegate_task`"),
);
NodeAssert.ok(orchestration.includes("never a `t3_thread_send`"));
NodeAssert.ok(orchestration.includes("not `t3_thread_send`"));
NodeAssert.ok(!orchestration.includes("inheritedOptions"));
NodeAssert.ok(!orchestration.includes("Prefer native"));
// The ACP terminal fallback is ACP-only; Codex agents never need it.
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/provider/agentContextBudget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"claudeSystemAppendBytes": 5594,
"codexAdditionalContextBytes": 7324,
"claudeSystemAppendBytes": 5604,
"codexAdditionalContextBytes": 7334,
"mcpToolListJsonBytes": 48094
}
2 changes: 2 additions & 0 deletions evals/mcp-tool-use/cases/orchestration.ndjson

Large diffs are not rendered by default.

28 changes: 24 additions & 4 deletions evals/mcp-tool-use/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import * as McpSchema from "effect/ai/McpSchema";
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";

import { taskPrompt } from "../../apps/server/src/mcp/delegatedTaskModes.ts";
import { delegatedCompletionWakeDetail } from "../../apps/server/src/orchestration-v2/delegatedCompletionWake.ts";
import * as McpInvocationContext from "../../apps/server/src/mcp/McpInvocationContext.ts";
import { advertisedJsonSchema } from "../../apps/server/src/mcp/advertisedJsonSchema.ts";
import * as Contracts from "@t3tools/contracts";
Expand Down Expand Up @@ -338,6 +339,17 @@ export const McpToolUseCase = Schema.Struct({
}),
/** Empty to resume a top-level thread without side-chat context after its last tool result. */
prompt: Schema.String,
/** Delegated tasks whose completion wakes the thread; T3's wake text replaces `prompt`. */
delegatedWake: Schema.optional(
Schema.Array(
Schema.Struct({
id: Schema.String,
title: Schema.NullOr(Schema.String),
status: Schema.Literals(["completed", "failed", "cancelled", "interrupted"]),
result: Schema.NullOr(Schema.String),
}),
),
),
/** Records for the context chips `prompt` links, projected as T3 sends them. */
promptContext: Schema.optional(Schema.Struct({ records: Schema.Array(ComposerContextRecord) })),
expect: McpToolUseExpect,
Expand All @@ -347,6 +359,14 @@ export const McpToolUseCase = Schema.Struct({
notes: Schema.optional(Schema.String),
});
export type McpToolUseCase = typeof McpToolUseCase.Type;

const casePrompt = ({ prompt, delegatedWake }: Pick<McpToolUseCase, "prompt" | "delegatedWake">) =>
delegatedWake === undefined
? prompt
: delegatedCompletionWakeDetail(
delegatedWake.map((task) => task.id),
delegatedWake,
);
const AnyBlock = Schema.StructWithRest(
Schema.Struct({ type: Schema.String, text: Schema.optional(Schema.String) }),
Rest,
Expand Down Expand Up @@ -1030,7 +1050,7 @@ export const buildMessages = (sample: Sample) => {
};
const { role, context, delegateRole, priorTurns = [] } = testCase.setup;
const sideChat = testCase.setup.sideChat;
if (testCase.prompt === "" && (role !== "top" || sideChat !== undefined)) {
if (casePrompt(testCase) === "" && (role !== "top" || sideChat !== undefined)) {
throw new Error(
`${testCase.id}: Empty-prompt replay requires a top-level thread without side-chat context.`,
);
Expand Down Expand Up @@ -1092,12 +1112,12 @@ export const buildMessages = (sample: Sample) => {
}
const userText =
testCase.promptContext === undefined
? testCase.prompt
? casePrompt(testCase)
: projectComposerContextForProvider({ text: testCase.prompt, ...testCase.promptContext });
const prompt =
sideChatContext === undefined ? userText : withInlineContext(sideChatContext, userText);
const childPrompt = () => taskPrompt({ task: prompt, role: delegateRole });
if (testCase.prompt !== "")
if (casePrompt(testCase) !== "")
push("user", text(role === "delegated-child" ? childPrompt() : prompt));
// Claude Code gives a subagent its SubagentStart hooks' context as a reminder after the task.
if (native && base.subagentContext) {
Expand Down Expand Up @@ -1524,7 +1544,7 @@ export const shellRequest = (sample: Sample, call: Omit<ShellEntry, "output">, m
]
: []),
...(context ? [`The agent was told: ${context}`] : []),
`The agent's task: ${testCase.prompt}`,
`The agent's task: ${casePrompt(testCase)}`,
...namedPathsLine(testCase),
];
const history = shell.map((entry) => `${callText(entry)}\n<output>\n${entry.output}\n</output>`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Ordinary coding tasks need no T3 tools.

- Subagents: use your native tool when the child uses your provider, a model it offers and no settings beyond yours (Claude \`Agent\`, \`run_in_background:false\`; Codex \`spawn_agent\`, then wait). Use \`delegate_task\` for another provider's model, user-asked settings, a separate checkout (\`isolation:"worktree"\`), or \`delegate_task\` by name. A native type on another provider's model counts too. Background agent: \`delegate_task\`, never a background \`Agent\`. Workflow only when asked.
- Call \`orchestrator_capabilities\` once per thread, before the first \`delegate_task\`. Again only after an error or a user-named provider change. Settings go in \`target.options\`. Provider or model unavailable: say so. Never switch unasked or fall back to native tools, Workflow or CLIs. Never make a child's checkout in the shell or in \`task\`.
- Every new delegate review or re-check is a new \`delegate_task\` (brief, findings, objections), never a \`t3_thread_send\`. Fix small review findings yourself; start no delegate for them. When told to proceed without a delegate, or to run fewer, \`task_cancel\` the extras. Start no replacement.
- Each new delegate review or re-check: a new \`delegate_task\` (brief, findings, objections), not \`t3_thread_send\`. Asked to continue an interrupted delegate: \`t3_thread_send\` to its \`childThreadId\`. Fix small findings yourself. Told to skip or cut delegates: \`task_cancel\` the extras. No replacements.
- Follow a running delegate's \`next\`. Don't poll it (\`task_status\`, \`sleep\`, watchers, Monitor, ScheduleWakeup). Stop a delegate run you replace or no longer need with \`task_cancel\`, never \`t3_thread_interrupt\` on a \`childThreadId\`.
- As a subagent or child, do the work yourself. Start no subagents unless your task says to.`,
`- \`t3_thread_launch\` and \`create_threads\` make new top-level threads, only when the user asks for a new thread. "Subagent", "delegate", and "parallel" never mean one. Worktree thread: set \`workspaceStrategy\`. Honor a requested base. Otherwise independent work uses the default branch as \`remote/branch\` with \`startFromOrigin:true\`; with no remote, use the local default. Required unpublished commits and dependent work use their local base. Shell worktrees don't bind it, and no checkout setup in its prompt. No \`workspaceStrategy\`: project root, not your worktree.
Expand Down
Loading