feat(agents): role orchestrateur du contexte global expose et configurable par agent — isOrchestrator distinct des permissions MCP, DTO update_project_orchestrator (terminologie wire legacy conservee), refus idea_context_update explicite avec writer effectif + orientation idea_context_propose, surface AgentsPanel + adaptateurs frontend — #276 (QA verte: cargo fmt/check, tests cibles application/app-tauri/infrastructure/backend, npm typecheck, 69/69 tests frontend agents/adapters)

This commit is contained in:
2026-09-07 22:49:12 +02:00
parent ad2c6b4354
commit cb5196fd69
24 changed files with 663 additions and 37 deletions

View File

@ -176,6 +176,18 @@ describe("TauriAgentGateway invoke payloads", () => {
});
});
it("update_project_orchestrator wraps the project and agent in the request DTO", async () => {
invoke.mockResolvedValueOnce({ orchestratorAgentId: "agent-2" });
await expect(
new TauriAgentGateway().updateProjectOrchestrator("proj-1", "agent-2"),
).resolves.toEqual({ orchestratorAgentId: "agent-2" });
expect(invoke).toHaveBeenCalledWith("update_project_orchestrator", {
request: { projectId: "proj-1", agentId: "agent-2" },
});
});
it("cancelAgentChat invokes cancel_agent_chat without closing the session", async () => {
await new TauriAgentGateway().cancelAgentChat("chat-session-1");

View File

@ -154,6 +154,16 @@ export class TauriAgentGateway implements AgentGateway {
});
}
updateProjectOrchestrator(
projectId: string,
agentId: string,
): Promise<{ orchestratorAgentId: string }> {
return invoke<{ orchestratorAgentId: string }>(
"update_project_orchestrator",
{ request: { projectId, agentId } },
);
}
readContext(projectId: string, agentId: string): Promise<string> {
return invoke<AgentContextDocument>("read_agent_context", {
projectId,

View File

@ -141,6 +141,26 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
});
});
it("updates the project orchestrator through the dedicated request DTO", async () => {
const response = { orchestratorAgentId: "agent-2" };
const { gw, calls } = httpAgentGateway(async () => ({
ok: true,
status: 200,
json: async () => response,
text: async () => JSON.stringify(response),
}));
await expect(
gw.updateProjectOrchestrator("proj-1", "agent-2"),
).resolves.toEqual(response);
const init = calls[0].init as { body: string };
expect(JSON.parse(init.body)).toEqual({
command: "update_project_orchestrator",
args: { request: { projectId: "proj-1", agentId: "agent-2" } },
});
});
it("launch → attached: conforming agent.launch frame, assignedConversationId consumed", async () => {
const { gw, sockets } = gateway();
const chunks: Uint8Array[] = [];

View File

@ -258,6 +258,15 @@ export class HttpAgentGateway implements AgentGateway {
request: { projectId, agentId, contactable },
});
}
updateProjectOrchestrator(
projectId: string,
agentId: string,
): Promise<{ orchestratorAgentId: string }> {
return this.http.invoke<{ orchestratorAgentId: string }>(
"update_project_orchestrator",
{ request: { projectId, agentId } },
);
}
readContext(projectId: string, agentId: string): Promise<string> {
return this.http
.invoke<AgentContextDocument>("read_agent_context", { projectId, agentId })

View File

@ -720,6 +720,24 @@ export class MockAgentGateway implements AgentGateway {
return structuredClone(list[idx]);
}
async updateProjectOrchestrator(
projectId: string,
agentId: string,
): Promise<{ orchestratorAgentId: string }> {
const list = this.getAgents(projectId);
if (!list.some((agent) => agent.id === agentId)) {
const err: GatewayError = {
code: "NOT_FOUND",
message: `agent ${agentId} not found in project ${projectId}`,
};
throw err;
}
for (let index = 0; index < list.length; index += 1) {
list[index] = { ...list[index], isOrchestrator: list[index].id === agentId };
}
return { orchestratorAgentId: agentId };
}
// ── Internal helpers for MockTemplateGateway (same-package use only) ──
/**