Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e01195b
πŸ€– feat: allow agentId on workspace tasks (default exec)
ThomasK33 Aug 25, 2026
931084c
πŸ€– fix: address Codex review on workspace-task agent overrides
ThomasK33 Aug 25, 2026
ce92a4a
πŸ€– fix: reachability-aware agent validation for workspace-turn targets
ThomasK33 Aug 25, 2026
ab12c42
πŸ€– fix: settle failed post-create agent validation through the handle …
ThomasK33 Aug 25, 2026
3dbef2e
πŸ€– fix: allow host-side (global/plugin) agents on unreachable target c…
ThomasK33 Aug 25, 2026
08bab92
πŸ€– fix: divergent-branch and plugin-defaults handling for agent launches
ThomasK33 Aug 25, 2026
2966fb3
πŸ€– fix: validate agents on the stream's discovery path; honest disposa…
ThomasK33 Aug 25, 2026
b2b5618
πŸ€– fix: fail closed for unreachable existing targets; sanitize branch …
ThomasK33 Aug 25, 2026
b267445
πŸ€– fix: verify owner branch for base vouching; cross-host and init-rac…
ThomasK33 Aug 25, 2026
4566a2c
πŸ€– fix: strict stream-time agent resolution backstop; vouched-only fat…
ThomasK33 Aug 25, 2026
de00023
πŸ€– fix: preserve strictAgentResolution across compaction and startup r…
ThomasK33 Aug 25, 2026
5b8ebd7
πŸ€– fix: honest disposable-cleanup wording; document launch AI-defaults…
ThomasK33 Aug 25, 2026
e572dac
πŸ€– fix: strict resolution also rejects definitions hidden after valida…
ThomasK33 Aug 25, 2026
f2e23b8
πŸ€– fix: exempt internal compact request from strict gate; verify exec …
ThomasK33 Aug 25, 2026
cd0f47e
πŸ€– fix: continue delegated turns under their own options on monitor wa…
ThomasK33 Aug 25, 2026
6f24c86
πŸ€– fix: tolerate partial history mocks in delegated-turn continuation …
ThomasK33 Aug 25, 2026
fae0deb
πŸ€– fix: reuse delegated-turn overrides only while the turn is still open
ThomasK33 Aug 25, 2026
24c27ce
πŸ€– fix: pin validated agent provenance; post-compaction wake carriers;…
ThomasK33 Aug 25, 2026
ae058f6
πŸ€– fix: pin exact definition source; require clean agent dirs for owne…
ThomasK33 Aug 25, 2026
47d1f97
πŸ€– fix: include gitignored files in cleanliness proof; defer misses wh…
ThomasK33 Aug 25, 2026
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
9 changes: 9 additions & 0 deletions src/common/orpc/schemas/agentDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,14 @@ export const AgentDefinitionPackageSchema = z
scope: AgentDefinitionScopeSchema,
frontmatter: AgentDefinitionFrontmatterSchema,
body: z.string(),
/**
* Exact source identity of the winning candidate: "built-in" for embedded
* definitions, otherwise the discovery root the file was read from (per-plugin
* agents dirs are unique per plugin). Scope alone is not a provenance
* identifier β€” project files and project plugins both report "project" β€” so
* strict explicit-agent sends pin this to detect a different definition
* taking over the same id between launch validation and streaming.
*/
source: z.string().optional(),
})
.strict();
30 changes: 29 additions & 1 deletion src/common/orpc/schemas/stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { AgentIdSchema } from "./agentDefinition";
import { AgentDefinitionScopeSchema, AgentIdSchema } from "./agentDefinition";
import { OpenAIReasoningModeSchema, ThinkingLevelSchema } from "../../types/thinking";
import { AgentModeSchema } from "../../types/mode";
import { ChatUsageDisplaySchema } from "./chatStats";
Expand Down Expand Up @@ -800,6 +800,34 @@ export const SendMessageOptionsSchema = z.object({
* iterating on agent files - a broken agent in the worktree won't affect message sending.
*/
disableWorkspaceAgents: z.boolean().optional(),
/**
* When truthy, a top-level send whose agentId cannot be resolved (or is hidden or
* disabled) at stream time fails loudly instead of silently falling back to exec.
* Workspace-turn launches with explicit agent overrides set this: pre-dispatch
* validation races init hooks and user edits, so stream-time resolution β€” which
* runs after initialization completes β€” is the last sound gate against running
* a different agent than the caller asked for. The object form additionally pins
* the validated definition's provenance: if the id resolves from a different
* scope than launch validation saw (e.g. a validated project shadow vanished and
* a global/built-in definition with the same id took over), the send fails
* instead of running a different prompt/tool policy. A single field (rather than
* a sibling flag) so every option-preservation path copies it verbatim.
*/
strictAgentResolution: z
.union([
z.boolean(),
z.object({
expectedScope: AgentDefinitionScopeSchema,
/**
* Exact source identity from AgentDefinitionPackage.source ("built-in" or the
* discovery root). Scope alone collapses distinct candidates (project files
* and project plugins both report "project"), so this pins the definition
* itself when known.
*/
expectedSource: z.string().optional(),
}),
])
.optional(),
/**
* Desktop/app-only capability: expose set_goal so an agent can create a
* continuation-backed goal for its current parent workspace. Headless callers
Expand Down
8 changes: 8 additions & 0 deletions src/common/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type PreservedSendOptions = Pick<
| "providerOptions"
| "experiments"
| "disableWorkspaceAgents"
| "strictAgentResolution"
| "allowAgentSetGoal"
| "skipAiSettingsPersistence"
>;
Expand All @@ -74,6 +75,10 @@ export function pickPreservedSendOptions(options: SendMessageOptions): Preserved
providerOptions: options.providerOptions,
experiments: options.experiments,
disableWorkspaceAgents: options.disableWorkspaceAgents,
// Delegated turns with explicit agent overrides must stay loud across the
// compaction replay too β€” dropping this would let the follow-up silently
// fall back to exec if the agent vanished in the meantime.
strictAgentResolution: options.strictAgentResolution,
Comment thread
ThomasK33 marked this conversation as resolved.
allowAgentSetGoal: options.allowAgentSetGoal,
skipAiSettingsPersistence: options.skipAiSettingsPersistence,
};
Expand All @@ -91,6 +96,7 @@ export type StartupRetrySendOptions = Pick<
| "providerOptions"
| "experiments"
| "disableWorkspaceAgents"
| "strictAgentResolution"
| "allowAgentSetGoal"
> & {
/** Correlation for a delegated workspace turn that must survive restart recovery. */
Expand Down Expand Up @@ -124,6 +130,8 @@ export function pickStartupRetrySendOptions(
providerOptions: options.providerOptions,
experiments: options.experiments,
disableWorkspaceAgents: options.disableWorkspaceAgents,
// Keep explicit-agent turns loud across restart recovery (see pickPreservedSendOptions).
strictAgentResolution: options.strictAgentResolution,
allowAgentSetGoal: options.allowAgentSetGoal,
...(workspaceTurnMuxMetadata != null ? { muxMetadata: workspaceTurnMuxMetadata } : {}),
...(agentInitiated === true ? { agentInitiated: true } : {}),
Expand Down
28 changes: 28 additions & 0 deletions src/common/utils/tools/toolDefinitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,34 @@ describe("TOOL_DEFINITIONS", () => {
}
});

it("accepts workspace task args with an agent id", () => {
const parsed = TaskToolArgsSchema.safeParse({
kind: "workspace",
agentId: "plan",
prompt: "Plan a small change",
title: "Plan dogfood",
});

expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.agentId).toBe("plan");
}
});

it("still rejects subagent_type for workspace tasks", () => {
const parsed = TaskToolArgsSchema.safeParse({
kind: "workspace",
subagent_type: "plan",
prompt: "Plan a small change",
title: "Plan dogfood",
});

expect(parsed.success).toBe(false);
if (!parsed.success) {
expect(parsed.error.issues[0]?.path).toEqual(["subagent_type"]);
}
});

it("rejects workspace task fanout until workspace handles support it", () => {
expect(
TaskToolArgsSchema.safeParse({
Expand Down
9 changes: 6 additions & 3 deletions src/common/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export function buildTaskToolDescription(runtimeMode: RuntimeMode | undefined):
"\n\nIMPORTANT: Whether a sub-agent can see uncommitted changes depends on the runtime. " +
`${getTaskRuntimeVisibilityGuidance(runtimeMode)} ` +
"\n\nProvide agentId (preferred) or subagent_type, prompt, title, run_in_background, and optional n. For sub-agents, use title as a short, friendly reusable role name (for example, Reviewer or Simplicity Auditor), not a task summary. For kind=workspace, use a normal work-specific chat title. " +
'For kind=workspace, agentId optionally selects the agent mode for the launched turn (for example "plan"); it defaults to exec, and internal agents are not eligible. ' +
"Use n only when you want several agents to try the same prompt independently. Omit it for a single task, and prefer non-interfering sub-agents for grouped runs (for example read-only agents like explore). " +
`\n\nA terminal report makes the child inactive but leaves its workspace persistent. Keep each parent's direct standalone bench small and role-based: aim for at most ${SUBAGENT_REUSABLE_BENCH_TARGET} and keep it below ${SUBAGENT_REUSABLE_BENCH_EXCLUSIVE_LIMIT}; deliberate grouped n runs are temporary exceptions. Before spawning standalone work, prefer reawakening a known inactive child when its context or expertise fits, and retitle it if its reusable responsibility changes. At the target, add a role only for a genuinely distinct responsibility and prune an inactive overlapping or least-useful role before reaching the limit. Reawakening preserves the child's checkout, so for repository-dependent work, reuse it only when that snapshot is appropriate or instruct the child to verify and synchronize before acting; otherwise spawn a new child. Stop active work with task_stop; use irreversible task_remove for consumed grouped candidates, bench consolidation, explicit user requests, or clearly obsolete contextβ€”not routine end-of-turn cleanup. ` +
"\n\nWhen the user explicitly asks for best-of-n work, the parent should begin with light preliminary analysis to extract shared context, constraints, or evaluation criteria that would otherwise be duplicated across children. " +
Expand Down Expand Up @@ -379,11 +380,13 @@ function refineTaskToolAgentArgs(
const hasSubagentType = typeof args.subagent_type === "string" && args.subagent_type.length > 0;

if (kind === "workspace") {
if (hasAgentId || hasSubagentType) {
// Workspace tasks accept agentId (agent mode for the launched turn, e.g. "plan") but keep
// rejecting the deprecated sub-agent alias subagent_type.
if (hasSubagentType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Workspace tasks do not accept agentId or subagent_type",
path: ["agentId"],
message: "Workspace tasks do not accept subagent_type",
path: ["subagent_type"],
});
}
if (args.n != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,9 @@ export async function readAgentDefinition(
scope: candidate.scope,
frontmatter: parsed.frontmatter,
body: parsed.body,
// Exact provenance for strict-send pinning; per-plugin candidate roots are
// unique per plugin, so root identity distinguishes file vs plugin sources.
source: candidate.root,
};

const validated = AgentDefinitionPackageSchema.safeParse(pkg);
Expand All @@ -677,7 +680,7 @@ export async function readAgentDefinition(
if (!skipScopes.has("built-in")) {
const builtIn = getBuiltInAgentDefinitions().find((pkg) => pkg.id === agentId);
if (builtIn) {
const validated = AgentDefinitionPackageSchema.safeParse(builtIn);
const validated = AgentDefinitionPackageSchema.safeParse({ ...builtIn, source: "built-in" });
if (!validated.success) {
throw new Error(
`Invalid built-in agent definition '${agentId}': ${validated.error.message}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export interface NodeAgentDefinitionContext {
runtime: Runtime;
workspacePath: string;
workspaceId: string;
/** agent-plugins experiment: also resolve definitions contributed by Agent Plugins. */
includeAgentPlugins?: boolean;
}

export interface ResolveNodeAgentAiSettingsParams {
Expand Down Expand Up @@ -134,14 +136,16 @@ async function loadDefinitionLayers(
const agentDefinition = await readAgentDefinition(
context.runtime,
context.workspacePath,
agentId
agentId,
{ includeAgentPlugins: context.includeAgentPlugins }
);
const chain = await resolveAgentInheritanceChain({
runtime: context.runtime,
workspacePath: context.workspacePath,
agentId: agentDefinition.id,
agentDefinition,
workspaceId: context.workspaceId,
includeAgentPlugins: context.includeAgentPlugins,
});

return collectDefinitionLayers(agentId, chain);
Expand Down
Loading
Loading