镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Open
4 changes: 3 additions & 1 deletion apps/mobile/src/features/threads/SubagentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
shouldShowInstanceBadge,
} from "@t3tools/client-runtime/state/provider-instance-display";
import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display";
import { subagentModelSelection } from "@t3tools/client-runtime/state/subagentRuntime";
import { formatSubagentEffort } from "@t3tools/client-runtime/state/thread-execution";
import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts";
import type { ReactNode } from "react";
Expand All @@ -25,6 +26,7 @@ type SubagentRowSubagent = Pick<
| "childThreadId"
| "model"
| "modelSelection"
| "origin"
| "driver"
| "providerInstanceId"
| "title"
Expand Down Expand Up @@ -134,7 +136,7 @@ function SubagentMetadata(props: {
parent ? scopeProjectRef(environmentId, parent.projectId) : null,
);
const childProject = useProject(child ? scopeProjectRef(environmentId, child.projectId) : null);
const selection = subagent.modelSelection;
const selection = subagentModelSelection(subagent, child);
const { modelLabel, modelSlug, workspace } = resolveSubagentMetadata({
model: subagent.model,
modelSelection: selection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function memberRowSubagent(
childThreadId: member.childThreadId as ThreadId | null,
model: member.model,
modelSelection: workflowMemberModelSelection(member, opening.providerInstanceId!),
origin: "provider_native" as const,
driver: opening.driver!,
providerInstanceId: opening.providerInstanceId!,
title: member.title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ describe("orchestrator MCP toolkit", () => {
(/FROM orchestration_v2_projection_runs\b/.test(query) &&
!query.includes("AND run_id IN")) ||
(/FROM orchestration_v2_projection_subagents\b/.test(query) &&
!query.includes("AND subagent_id IN")),
!/AND (?:subagent\.)?subagent_id IN\b/.test(query)),
),
).toEqual([]);
const afterRead = yield* orchestrator.getThreadProjection(parentThreadId);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8829,6 +8829,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
prompt: command.task,
title: command.title ?? null,
model: command.modelSelection.model,
modelSelection: command.modelSelection,
...(command.completionWake === undefined ? {} : { completionWake: command.completionWake }),
status: "running",
result: null,
Expand Down Expand Up @@ -9258,6 +9259,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
prompt: command.task,
title: command.title ?? null,
model: command.modelSelection.model,
modelSelection: command.modelSelection,
...(command.completionWake === undefined ? {} : { completionWake: command.completionWake }),
status: "running",
result: null,
Expand Down
67 changes: 66 additions & 1 deletion apps/server/src/orchestration-v2/ProjectionStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1747,7 +1747,7 @@ const commandReadsSkipSettledLeafNodes = Effect.gen(function* () {
nativeTaskRef: null,
prompt: "Do the work",
title: name,
model: null,
model: name === "delivered" ? modelSelection.model : null,
...(delivery === null
? {}
: { completionDelivery: { state: delivery, observedByRunId: null } }),
Expand All @@ -1772,6 +1772,33 @@ const commandReadsSkipSettledLeafNodes = Effect.gen(function* () {
subagentNames(yield* store.getThreadRecords(threadId, ["subagents"])),
subagents.map(([name]) => name).toSorted(),
);
const childSelection = {
...modelSelection,
options: [{ id: "thinking", value: "high" }],
};
const parent = (yield* store.getThreadProjection(threadId)).thread;
yield* store.apply({
id: EventId.make("event:settled-subagents:child-created"),
type: "thread.created",
threadId: childThreadIds.delivered!,
occurredAt: now,
payload: { ...parent, id: childThreadIds.delivered!, modelSelection: childSelection },
});
for (const records of [
yield* store.getThreadRecords(threadId, ["subagents"], {
subagentIds: [NodeId.make("node:settled-subagents:delivered")],
}),
yield* store.getThreadRecords(threadId, ["subagents"]),
(yield* store.getThreadSnapshotWindow(threadId, { rowLimit: 76 })).projection,
]) {
assert.deepEqual(
records.subagents.find((agent) => agent.title === "delivered")?.modelSelection,
childSelection,
);
assert.isUndefined(
records.subagents.find((agent) => agent.title === "no-delivery")?.modelSelection,
);
}
// A command about one settled task keeps it, and reads only its node and turn item.
const deliveredId = NodeId.make("node:settled-subagents:delivered");
const forTask = yield* store.getThreadRecords(threadId, ["subagents", "nodes", "turnItems"], {
Expand All @@ -1797,6 +1824,44 @@ const commandReadsSkipSettledLeafNodes = Effect.gen(function* () {
[...(yield* store.listAppOwnedChildThreadIds(threadId))],
[childThreadIds.delivered, childThreadIds["no-delivery"]],
);
const delivered = forTask.subagents.find((agent) => agent.id === deliveredId)!;
const reportedSelection = { ...modelSelection, options: [{ id: "thinking", value: "low" }] };
for (const [origin, instanceId, model] of [
["provider_native", providerInstanceId, modelSelection.model],
["app_owned", ProviderInstanceId.make("other-instance"), modelSelection.model],
["app_owned", providerInstanceId, "older-model"],
] as const) {
yield* store.apply({
id: EventId.make(`event:settled-subagents:guard:${origin}`),
type: "subagent.updated",
threadId,
nodeId: deliveredId,
driver,
providerInstanceId: instanceId,
occurredAt: now,
payload: {
...delivered,
origin,
providerInstanceId: instanceId,
model,
modelSelection: { ...reportedSelection, instanceId, model },
},
});
for (const records of [
yield* store.getThreadRecords(threadId, ["subagents"], { subagentIds: [deliveredId] }),
yield* store.getThreadRecords(threadId, ["subagents"]),
(yield* store.getThreadSnapshotWindow(threadId, { rowLimit: 76 })).projection,
]) {
assert.deepEqual(
records.subagents.find((agent) => agent.id === deliveredId)?.modelSelection,
{
...reportedSelection,
instanceId,
model,
},
);
}
}
});

const backgroundThreadUpdatesKeepTimestamp = Effect.gen(function* () {
Expand Down
38 changes: 27 additions & 11 deletions apps/server/src/orchestration-v2/ProjectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3432,6 +3432,17 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
},
};
});
// Settled children leave the shell stream, so their own selection travels with the task.
const subagentPayload = sql`CASE
WHEN subagent.origin = 'app_owned'
AND child.provider_instance_id = subagent.provider_instance_id
AND (json_extract(subagent.payload_json, '$.model') IS NULL
OR json_extract(child.payload_json, '$.modelSelection.model')
= json_extract(subagent.payload_json, '$.model'))
THEN json_set(subagent.payload_json, '$.modelSelection',
json_extract(child.payload_json, '$.modelSelection'))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
ELSE subagent.payload_json
END AS payload_json`;
const cohortJson = (field: string) =>
JSON.stringify(
turnItems.flatMap((payload) => {
Expand Down Expand Up @@ -3679,11 +3690,13 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
? Effect.succeed([])
: window === undefined
? sql<PayloadRow>`
SELECT payload_json
SELECT ${subagentPayload}
FROM orchestration_v2_projection_subagents AS subagent
WHERE ${threadColumn(filter?.subagentIds)} = ${threadId}
${filter?.subagentIds === undefined ? sql`` : sql`AND subagent_id IN (SELECT value FROM json_each(${encodeIdList(filter.subagentIds)}))`}
${filter?.subagentRunIds === undefined ? sql`` : sql`AND run_id IN (SELECT value FROM json_each(${encodeIdList(filter.subagentRunIds)}))`}
LEFT JOIN orchestration_v2_projection_threads AS child
ON child.thread_id = subagent.child_thread_id
WHERE ${threadColumn(filter?.subagentIds, "subagent.thread_id")} = ${threadId}
${filter?.subagentIds === undefined ? sql`` : sql`AND subagent.subagent_id IN (SELECT value FROM json_each(${encodeIdList(filter.subagentIds)}))`}
${filter?.subagentRunIds === undefined ? sql`` : sql`AND subagent.run_id IN (SELECT value FROM json_each(${encodeIdList(filter.subagentRunIds)}))`}
${
filter?.omitSettledSubagents !== undefined
? sql`AND (${
Expand All @@ -3702,15 +3715,18 @@ export const layer: Layer.Layer<ProjectionStoreV2, never, SqlClient.SqlClient> =
))`
: sql``
}
ORDER BY COALESCE(started_at, ''), subagent_id ASC
ORDER BY COALESCE(subagent.started_at, ''), subagent.subagent_id ASC
`
: sql<PayloadRow>`
SELECT payload_json FROM orchestration_v2_projection_subagents
WHERE thread_id = ${threadId}
AND (status IN ('pending','starting','running','waiting')
OR run_id IN (SELECT value FROM json_each(${cohortRunIds}))
OR parent_node_id IN (SELECT value FROM json_each(${cohortNodeIds})))
ORDER BY COALESCE(started_at, ''), subagent_id ASC
SELECT ${subagentPayload}
FROM orchestration_v2_projection_subagents AS subagent
LEFT JOIN orchestration_v2_projection_threads AS child
ON child.thread_id = subagent.child_thread_id
WHERE subagent.thread_id = ${threadId}
AND (subagent.status IN ('pending','starting','running','waiting')
OR subagent.run_id IN (SELECT value FROM json_each(${cohortRunIds}))
OR subagent.parent_node_id IN (SELECT value FROM json_each(${cohortNodeIds})))
ORDER BY COALESCE(subagent.started_at, ''), subagent.subagent_id ASC
`,
fields !== undefined && !fields.includes("providerSessions")
? Effect.succeed([])
Expand Down
18 changes: 6 additions & 12 deletions apps/web/src/components/chat/SubagentTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
resolveSubagentMetadata,
subagentDetailPreview,
} from "@t3tools/client-runtime/state/subagent-display";
import { REASONING_EFFORT_OPTION_IDS } from "@t3tools/client-runtime/state/thread-execution";
import { getModelSelectionStringOptionValue, resolveSelectableModel } from "@t3tools/shared/model";
import { subagentModelSelection } from "@t3tools/client-runtime/state/subagentRuntime";
import { formatSubagentEffort } from "@t3tools/client-runtime/state/thread-execution";
import { resolveSelectableModel } from "@t3tools/shared/model";
import { getTraitsSpeedDisplay, TraitsSpeedIcon } from "./TraitsSpeed";
import type { ReactNode } from "react";
import {
Expand Down Expand Up @@ -50,9 +51,7 @@ export function SubagentTooltipContent(props: {
result?: string | null | undefined;
progress?: string | null | undefined;
}) {
const childSelection =
props.modelSelection ??
(props.origin === "app_owned" ? props.childThread?.modelSelection : undefined);
const childSelection = subagentModelSelection(props, props.childThread);
const {
modelLabel,
modelSlug,
Expand All @@ -68,13 +67,8 @@ export function SubagentTooltipContent(props: {
) ?? childSelection?.model.trim())
: childSelection?.model.trim();
const provider = props.provider;
const matchingSelection =
childModel === modelSlug && childSelection?.instanceId === props.providerInstanceId
? childSelection
: undefined;
const effort = REASONING_EFFORT_OPTION_IDS.map((id) =>
getModelSelectionStringOptionValue(matchingSelection, id),
).find(Boolean);
const matchingSelection = childModel === modelSlug ? childSelection : undefined;
const effort = formatSubagentEffort({ modelSlug, modelSelection: childSelection, provider });
const speed = provider
? providerModel?.capabilities?.optionDescriptors
?.map((descriptor) => {
Expand Down
7 changes: 3 additions & 4 deletions packages/client-runtime/src/handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,9 @@ function resolveEffort(
): { readonly value: string; readonly label: string } | undefined {
const model = models?.find((candidate) => candidate.slug === selection.model);
if (model === undefined) {
// Pi states its effort as `thinking`; the catalog would otherwise mark it a reasoning control.
const value = [...REASONING_EFFORT_OPTION_IDS, "thinking"]
.map((id) => getModelSelectionStringOptionValue(selection, id))
.find((candidate) => candidate !== undefined);
const value = REASONING_EFFORT_OPTION_IDS.map((id) =>
getModelSelectionStringOptionValue(selection, id),
).find((candidate) => candidate !== undefined);
return value === undefined ? undefined : { value, label: value };
}
const descriptor = model.capabilities?.optionDescriptors?.find(isReasoningSelect);
Expand Down
5 changes: 1 addition & 4 deletions packages/client-runtime/src/modelTraits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import {
} from "@t3tools/shared/model";
import { REASONING_EFFORT_OPTION_IDS } from "./state/threadExecution.ts";

const REASONING_DESCRIPTOR_IDS: ReadonlySet<string> = new Set([
...REASONING_EFFORT_OPTION_IDS,
"thinking",
]);
const REASONING_DESCRIPTOR_IDS: ReadonlySet<string> = new Set(REASONING_EFFORT_OPTION_IDS);

/** The model's reasoning control: a known effort id, or a select its agent marked as reasoning. */
export function isReasoningControl(descriptor: ProviderOptionDescriptor): boolean {
Expand Down
36 changes: 36 additions & 0 deletions packages/client-runtime/src/state/subagentRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isWorkflowScriptMessage,
projectedSubagentsToRuntime,
resolveWorkflowOpening,
subagentModelSelection,
workflowMemberModelSelection,
} from "./subagentRuntime.ts";
import { resolveSubagentMetadata } from "./subagentDisplay.ts";
Expand Down Expand Up @@ -112,6 +113,41 @@ describe("projectedSubagentsToRuntime workflow expansion", () => {
expect(formatSubagentEffort({ modelSlug, modelSelection, provider })).toBe("xhigh");
});

it("picks a delegated child's own saved selection, never the parent's or a guess", () => {
const instanceId = ProviderInstanceId.make("pi");
const model = "opencode-go/deepseek-v4.1-flash";
const saved = { instanceId, model, options: [{ id: "thinking", value: "high" }] };
const reported = { instanceId, model, options: [{ id: "thinking", value: "max" }] };
const delegated = {
origin: "app_owned",
model,
providerInstanceId: instanceId,
modelSelection: reported,
} as const;
const movedChild = {
modelSelection: { ...saved, instanceId: ProviderInstanceId.make("pi-work") },
};

expect(subagentModelSelection(delegated, { modelSelection: saved })).toBe(saved);
expect(subagentModelSelection({ ...delegated, model: null }, { modelSelection: saved })).toBe(
saved,
);
expect(subagentModelSelection(delegated, undefined)).toBe(reported);
expect(subagentModelSelection(delegated, movedChild)).toBe(reported);
expect(
subagentModelSelection(delegated, { modelSelection: { ...saved, model: "new-model" } }),
).toBe(reported);
expect(
subagentModelSelection({ ...delegated, modelSelection: undefined }, undefined),
).toBeUndefined();
expect(
subagentModelSelection(
{ ...delegated, origin: "provider_native" },
{ modelSelection: saved },
),
).toBe(reported);
});

it("names a workflow by the model it runs on when the provider reports none", () => {
const [workflow] = projectedSubagentsToRuntime([subagent({ model: null })]);
expect(
Expand Down
20 changes: 20 additions & 0 deletions packages/client-runtime/src/state/subagentRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,26 @@ export function workflowMemberModelSelection(
};
}

/**
* The selection a subagent runs on: a T3 delegated child's saved selection first, then what was
* reported for the subagent itself, on its own provider instance and known model. Never the parent's.
*/
export function subagentModelSelection(
subagent: Pick<
OrchestrationV2Subagent,
"origin" | "model" | "modelSelection" | "providerInstanceId"
>,
childThread: Pick<OrchestrationV2AppThread, "modelSelection"> | null | undefined,
): ModelSelection | undefined {
// A provider-native child thread is not where its subagent's selection is saved.
const saved = subagent.origin === "app_owned" ? childThread?.modelSelection : undefined;
return [saved, subagent.modelSelection].find(
(selection) =>
selection?.instanceId === subagent.providerInstanceId &&
(subagent.model === null || selection.model === subagent.model),
);
}

export function projectedSubagentsToRuntime(
subagents: ReadonlyArray<{
readonly id: string;
Expand Down
Loading
Loading