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
4 changes: 2 additions & 2 deletions FORK.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions apps/server/src/j5/a2a/crewGateNotice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,8 @@ export const crewLaunchReportText = (input: {
prose.push(
"Your crew is running. Each seat has your brief and this roster; coordinate with send_message, and ask the user through the inbox for decisions you cannot make from the brief.",
);
prose.push(
"Seats are told to message you with their results; do not ask for the same results again. End your turn and read them as they arrive.",
);
return `${block}\n\n${prose.join(" ")}`;
};
4 changes: 2 additions & 2 deletions apps/server/src/j5/a2a/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,10 +384,10 @@ export const J5_LIST_AGENTS_DESCRIPTION =
"List the personas in this environment: id, purpose, runtime policy, whether each can start now, and the provider, model, and reasoning it would run on. Read this before choosing a persona for spawn_agent or a crew roster so the choice fits the task and the user's budget. Read-only.";

export const J5_PROPOSE_CREW_DESCRIPTION =
"Propose the crew you need for the brief you were given. Use it when the user asks for a crew or the work splits into distinct responsibilities that should run at once. Call list_personas first and pick one persona per seat, or leave persona unset for a custom seat that runs on your own provider, model, and access mode with only its instructions (required) and the brief; name the crew for what it is for and give each seat a short lowercase-hyphen name like code-reviewer. The user reviews the roster in this thread, may remove or add seats, and approves or declines; you receive the decision and the roster as a message here. Approved seats run with their own persona's permissions, which may exceed yours. You become the crew's Captain and may command several crews at once; later requests, stops, and archives name the crew they mean. Reuse client_request_id to retry safely. This call is itself the human gate, so it works under every sandbox and approval policy, including approval policy never; never refuse the brief because approvals are disabled.";
"Propose the crew you need for the brief you were given. Use it when the user asks for a crew or the work splits into distinct responsibilities that should run at once. Mix saved personas and custom seats in the same roster: call list_personas when choosing a saved persona, or leave persona unset for a custom seat that runs on your own provider, model, and access mode with only its instructions (required) and the brief; name the crew for what it is for and give each seat a short lowercase-hyphen name like code-reviewer. The user reviews the roster and each seat's resolved provider, model, reasoning, and access in this thread, may remove or add seats, and approves or declines; you receive the decision and the roster as a message here. Approved seats run with their own persona's permissions, which may exceed yours. You become the crew's Captain and may command several crews at once; later requests, stops, and archives name the crew they mean. Use send_message for member-to-member, member-to-Captain, and Captain-to-Captain coordination, including intermediate findings and direct results; artifacts do not gate these conversations. Reuse client_request_id to retry safely. This call is itself the human gate, so it works under every sandbox and approval policy, including approval policy never; never refuse the brief because approvals are disabled.";

export const J5_REQUEST_CREW_MEMBER_DESCRIPTION =
"Ask the user to add one seat to a crew you command when the work needs one the roster lacks: seat name, persona id from list_personas (or none for a custom seat that runs on your provider, model, and access and needs instructions), a one-line reason, and optionally instructions and a brief for the new seat. The user decides from their inbox; you receive the decision and the updated roster as a message here and can keep working meanwhile. Captain-only; a member escalates to its Captain. Reuse client_request_id to retry safely. Filing the request is the human gate itself and works under every approval policy, including approval policy never.";
"Ask the user to add one seat to a crew you command when the work needs one the roster lacks: seat name, persona id from list_personas (or none for a custom seat that runs on your provider, model, and access and needs instructions), a clear reason identifying the concern and missing expertise or responsibility, and optionally instructions and a brief for the new seat. The user decides from their inbox; you receive the decision and the updated roster as a message here. Continue the already-approved work and direct coordination while the addition is pending. Captain-only; a member sends the concern and needed expertise to its Captain with send_message. Reuse client_request_id to retry safely. Filing the request is the human gate itself and works under every approval policy, including approval policy never.";

export const J5_STOP_AGENT_DESCRIPTION =
"Stop one Peer Agent: interrupts its running turn now. The agent remains, stays readable, and can be messaged again later — stopping halts work, it retires nothing. Requires your current squadron_id. Reuse client_request_id to retry safely.";
Expand Down
57 changes: 57 additions & 0 deletions apps/server/src/j5/a2a/spawnIds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,60 @@ describe("spawn thread ids", () => {
);
});
});

import { assert } from "@effect/vitest";

import { spawnFirstTurnText, type CrewBriefContext } from "./spawnIds.ts";

const identity = {
brief: "Review the proposed change.",
participantId: "agent:reviewer",
squadronId: "squadron:review",
squadronName: "Review",
};
const crew: CrewBriefContext = {
displayName: "Change review",
instanceId: "crew:review",
seatName: "reviewer",
captainParticipantId: "agent:captain",
seatInstructions: "Review correctness and report concrete risks.",
roster: [
{ seat: "reviewer", participantId: identity.participantId, agentDisplayName: "custom" },
{ seat: "builder", participantId: "agent:builder", agentDisplayName: "Builder" },
],
};

it("gives custom seats direct result and concern reporting without a mandatory artifact", () => {
const text = spawnFirstTurnText({ ...identity, crew });
assert.include(text, "captain_participant_id: agent:captain");
assert.include(text, "- builder: participant_id=agent:builder");
assert.include(
text,
"use send_message to coordinate directly with your Captain and other members",
);
assert.include(text, "final result, supporting evidence, and any remaining blockers");
assert.include(text, "request_crew_member through the user's inbox");
assert.include(text, "Continue already-approved work and coordination");
assert.include(text, "A direct result is sufficient");
assert.include(text, `<seat_instructions>\n${crew.seatInstructions}\n</seat_instructions>`);
assert.notInclude(text, "<seat_obligation>");
assert.notInclude(text, "write_artifact");
});

it("retains declared persona output while allowing conversation before that output exists", () => {
const text = spawnFirstTurnText({
...identity,
crew: { ...crew, obligation: { kind: "ReviewHandoff", path: "handoffs/review.md" } },
});
assert.include(text, "do not wait for an artifact or coordination approval");
assert.include(text, "write_artifact to exactly `handoffs/review.md`");
assert.include(text, "must not delay sharing intermediate findings or results");
assert.notInclude(text, "a chat message is not a delivery");
});

it("keeps an ordinary Peer Agent brief free of crew instructions", () => {
const text = spawnFirstTurnText(identity);
assert.include(text, `<spawner_brief>\n${identity.brief}\n</spawner_brief>`);
assert.notInclude(text, "crew_collaboration");
assert.notInclude(text, "Captain");
});
10 changes: 6 additions & 4 deletions apps/server/src/j5/a2a/spawnIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,15 @@ export const spawnFirstTurnText = (input: {
crew.seatInstructions === undefined
? []
: [`<seat_instructions>\n${crew.seatInstructions}\n</seat_instructions>`];
// The deliverable is a platform obligation, not wiring prose: it is the same handoff file every
// saved agent writes, and the Captain is told the moment it appears.
const collaboration = `<crew_collaboration>\nResolve current participants with list_participants and use send_message to coordinate directly with your Captain and other members. Share useful findings, evidence, questions, and blockers immediately; do not wait for an artifact or coordination approval. Send your Captain your final result, supporting evidence, and any remaining blockers before finishing. If the roster lacks expertise needed to address a concern, tell your Captain what is missing and why so they can request_crew_member through the user's inbox. Continue already-approved work and coordination while an addition is pending. A direct result is sufficient unless the user or your persona explicitly requires an artifact.\n</crew_collaboration>`;
// A persona's declared deliverable supplements the conversation; it never gates coordination.
const obligation =
crew.obligation === undefined
? []
: [
`<seat_obligation>\nYour agent definition returns a ${crew.obligation.kind}. Before you finish, write it with write_artifact to exactly \`${crew.obligation.path}\` as Markdown (your instructions list the required contents); a chat message is not a delivery. Your Captain is told when the file appears; rewrite the same path to revise it.\n</seat_obligation>`,
`<seat_obligation>\nYour agent definition returns a ${crew.obligation.kind}. Before you finish, write it with write_artifact to exactly \`${crew.obligation.path}\` as Markdown (your instructions list the required contents); this is in addition to direct messages and must not delay sharing intermediate findings or results. Your Captain is told when the file appears; rewrite the same path to revise it.\n</seat_obligation>`,
];
return [identity, crewContext, ...seatInstructions, ...obligation, brief].join("\n\n");
return [identity, crewContext, collaboration, ...seatInstructions, ...obligation, brief].join(
"\n\n",
);
};
22 changes: 22 additions & 0 deletions apps/server/src/provider/T3OrchestrationInstructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ describe("T3 orchestration provider instructions", () => {
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "bindToCurrentThread=false");
});

it("creates mixed crews from chat and keeps coordination independent of approvals and artifacts", () => {
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "asks for a crew in ordinary chat");
assert.include(
T3_CODE_ORCHESTRATION_INSTRUCTIONS,
"saved personas from `list_personas` with custom seats",
);
assert.notInclude(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "`list_agents`");
assert.include(
T3_CODE_ORCHESTRATION_INSTRUCTIONS,
"resolved provider, model, reasoning, and access",
);
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "Captains of other crews");
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "Do not wait for an artifact");
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "`request_crew_member`");
assert.include(
T3_CODE_ORCHESTRATION_INSTRUCTIONS,
"reason naming the concern and needed responsibility",
);
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "while that request is pending");
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "otherwise they queue for its next turn");
});

it("routes durable planning outputs into artifacts and excludes working files", () => {
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "durable, user-consumable planning outputs");
assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "`write_artifact`");
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/provider/T3OrchestrationInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ The \`t3-code\` MCP server provides app-owned orchestration. When you need other
${AGENT_INVOCATION_INSTRUCTIONS}
- A provider-native Subagent is child work created and owned inside one provider session. Use your provider's native Subagent mechanism when the user asks for a subagent, or when you want help the user does not need to see or message. T3 observes provider-native Subagents; the persona mention above instead creates a T3-owned delegated child. For cross-provider work that the user does not need to message directly, use \`delegate_task\`.
- A Peer Agent is a full participant with its own top-level thread that the user can open and message. Use platform \`spawn_agent\` when the user wants or would benefit from talking to the agent directly, including cross-provider work where the user needs a directly messageable participant. Its brief states the task and whether a reply is expected; when you need a reply, include what should come back in that brief instead of sending a follow-up ask.
- A Crew is a group of Peer Agents you command for one bounded task; the user can message any seat but mostly speaks through you. Use \`propose_crew\` when the user asks for a crew or the work splits into distinct responsibilities that should run at once: pick seats from \`list_agents\`, and the user approves, edits, or declines the roster in the app. Approved seats spawn under you, and each seat's finish reaches you as a message.
- Use \`list_participants\` to resolve an already-addressable agent or the human. Only for later work owed by an existing participant, use \`send_message(..., expect_reply=true, intent="...")\` to open an Exchange. The reply arrives later as an incoming message; continue with other work instead of polling.
- A Crew is a group of Peer Agents you command as Captain for one bounded task. Use \`propose_crew\` when the user asks for a crew in ordinary chat or the work splits into distinct responsibilities that should run at once. Mix saved personas from \`list_personas\` with custom seats: set persona for a saved persona, or omit it and provide task-specific instructions for a custom seat. The app shows each seat's resolved provider, model, reasoning, and access before the user approves, edits, or declines the roster. Approved seats spawn under you, and each seat's finish reaches you as a message.
- Crew members, their Captain, and Captains of other crews can coordinate directly with \`send_message\`: share intermediate findings, concerns, questions, evidence, and final results as soon as useful. Do not wait for an artifact or a separate coordination approval. Return results in messages unless the user or a selected persona explicitly requires an artifact; that output requirement never blocks conversation. If a concern needs expertise missing from the roster, the member tells its Captain, and the Captain uses \`request_crew_member\` with the crew_instance_id and a clear reason naming the concern and needed responsibility. The user approves additions in the existing inbox; continue the already-approved work and coordination while that request is pending.
- Use \`list_participants\` to resolve an already-addressable agent or the human. Use \`send_message\` without expect_reply for updates or results that need no response. Only for later work owed by an existing participant, use \`send_message(..., expect_reply=true, intent="...")\` to open an Exchange. The reply arrives later as an incoming message; continue with other work instead of polling or holding a turn open to wait. Messages reach a supported active turn through provider steering; otherwise they queue for its next turn.
- \`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. By default runs return to the current thread; set \`bindToCurrentThread=false\` only when the user wants a fresh thread for every run. After scheduling, report the returned cadence and next run time.
- Use \`write_artifact\` for durable, user-consumable planning outputs that should remain available across threads and agents in the project, such as plans, specifications, diagrams, and research notes. Use \`list_artifacts\` and \`read_artifact\` to access existing artifacts. Do not use artifacts for source code, build output, logs, temporary scratch files, or ordinary repository documentation. Refer to a saved artifact in chat by the logical path returned by \`write_artifact\` (for example \`artifacts/plan.md\`) so J5 opens it in the Artifacts panel. J5 stores artifacts in application data, outside the repository. Artifacts under \`handoffs/\` are versioned: rewriting one adds your content as a new version at the top of the same file rather than replacing it, and reading it back returns a versions header followed by every kept version, newest first.

Expand Down
18 changes: 1 addition & 17 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,6 @@ import {
startSquadronDraft,
} from "../j5/squadron/SquadronPicker.logic";
import { refreshThreadHomes, useThreadHomes } from "../j5/squadron/ThreadHomesClient";
import { crewCommandRefusal, crewLaunchPrompt, parseCrewCommand } from "../j5/crew/crewCommand";
import {
resolveEffectiveSquadronId,
resolveSquadronDraftChipState,
Expand Down Expand Up @@ -7027,17 +7026,6 @@ export default function ChatView(props: ChatViewProps) {
}
}

// J5: `/crew <brief>` sends the brief wrapped in the crew-composition guidance as this
// thread's next turn, whatever agent the thread runs as. Parsed from the raw prompt (attached
// contexts are not a brief) and refused before any in-flight state is set, so a refusal leaves
// the composer exactly as it was.
const j5Crew = parseCrewCommand(promptForSend);
const j5CrewRefusal = j5Crew === null ? null : crewCommandRefusal(j5Crew);
if (j5CrewRefusal !== null) {
toastManager.add(stackedThreadToast({ type: "warning", ...j5CrewRefusal }));
return;
}

sendInFlightRef.current = true;
if (isDraftHeroState && activeThreadKey) {
let resolveDockStarted: (() => void) | undefined;
Expand All @@ -7063,12 +7051,8 @@ export default function ChatView(props: ChatViewProps) {
const composerElementContextsSnapshot = [...composerElementContexts];
const composerPreviewAnnotationsSnapshot = [...composerPreviewAnnotations];
const composerReviewCommentsSnapshot: ReviewCommentContext[] = [...composerReviewComments];
// J5: a `/crew` turn sends the wrapped brief in place of the raw command; the attached
// contexts below still ride along, so the Captain reads them with the brief.
const promptBodyForSend =
j5Crew?.kind === "launch" ? crewLaunchPrompt(j5Crew.brief) : promptForSend;
const messageTextWithContexts = appendElementContextsToPrompt(
appendTerminalContextsToPrompt(promptBodyForSend, composerTerminalContextsSnapshot),
appendTerminalContextsToPrompt(promptForSend, composerTerminalContextsSnapshot),
composerElementContextsSnapshot,
);
const messageTextWithPreviewAnnotations = composerPreviewAnnotationsSnapshot.reduce(
Expand Down
8 changes: 1 addition & 7 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,6 @@ import { resolveComposerDispatchMode, type ComposerDispatchMode } from "./compos
import { SteerUnavailableNotice } from "../../j5/composer/SteerUnavailableNotice";
import { useJ5SteerState } from "../../j5/composer/useJ5SteerState";
import { shouldRefuseComposerSteer } from "../../j5/composer/submitGuard";
import { j5CrewSlashCommandItems } from "../../j5/crew/crewSlashCommand";
import type { ContextWindowSnapshot } from "../../lib/contextWindow";
import {
formatProviderSkillDisplayName,
Expand Down Expand Up @@ -2026,12 +2025,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
(item) => item.command.name !== "compact" || compactSlashCommandAvailable,
);
const slashCommandItems = slashCommandItemsForPromptPosition(
[
...builtInSlashCommandItems,
...j5CrewSlashCommandItems(selectedProvider),
...visibleProviderSlashCommandItems,
...skillItems,
],
[...builtInSlashCommandItems, ...visibleProviderSlashCommandItems, ...skillItems],
composerTrigger.rangeStart === 0,
);
return searchSlashCommandItems(slashCommandItems, query);
Expand Down
Loading
Loading