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
8 changes: 8 additions & 0 deletions backend/src/apis/shared/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ def agents_enabled() -> bool:
admins until the marketplace went GA, and that condition came off with D14 (the nav
entry is gated on this flag alone). See ``agent_marketplace_enabled`` for why there
is no RBAC capability on this axis.

⚠️ **The kill switch's meaning changed in Designer Phase 5.** While the SPA shipped
both nouns, turning this off degraded gracefully: the Agents nav disappeared and the
Assistants editor was still there. Phase 5 retired that editor and redirected
``/assistants*`` onto the Agent surface, so there is nothing left to fall back to —
off now means *no authoring surface at all*, not *the previous one*. Treat it as an
outage switch, not a feature toggle. (The records are untouched either way; the
routes and the SPA pages are what disappear.)
"""
return os.environ.get("AGENTS_API_ENABLED", "").strip().lower() != "false"

Expand Down
33 changes: 25 additions & 8 deletions docs/specs/agent-designer.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,24 +182,41 @@ definition.

## Migration / Assistant deprecation

1. Evolve the store (D2) + ship the Agent contract with the compat mapping.
2. Build the Designer to parity, then past it (tools/skills/memory binding old Assistants never had).
3. Render legacy Assistants as Agents; redirect the Assistants editor to the Designer.
4. Deprecate the "Assistant" term across UI + docs; retire the old editor.
1. ✅ Evolve the store (D2) + ship the Agent contract with the compat mapping.
2. ✅ Build the Designer to parity, then past it (tools/skills/memory binding old Assistants never had).
3. ✅ Render legacy Assistants as Agents; redirect the Assistants editor to the Designer.
4. 🔄 Deprecate the "Assistant" term across UI + docs; retire the old editor.

No big-bang: legacy ids and the compat mapping keep everything running throughout.

**Step 3, as shipped.** `/assistants`, `/assistants/new` and `/assistants/:id/edit` are
`redirectTo` entries onto their `/agents` equivalents rather than deletions. Those paths are in
bookmarks, in the "edit" link of every old chat session, and in links people shared with each
other; because the ids are the same record on both sides, the redirect lands on exactly what the
old URL opened. The sidenav ships one entry, and the Agents "Preview" badge came off with the
second noun it existed to disambiguate.

⚠️ **This changed what the `AGENTS_API_ENABLED` kill switch means.** While both nouns shipped,
turning it off degraded to the Assistants editor. There is no longer anything to fall back to, so
off now means no authoring surface at all. It is an outage switch, not a feature toggle — records
are untouched either way.

**The term pass is deliberately not a find-and-replace.** `"You are a helpful assistant that…"`
stays as the instructions placeholder: that is the conventional system-prompt idiom, and rewriting
it to "agent" would be worse prompt guidance, not better terminology. What changed is the words
naming *our product concept* — nav, the session indicator, the share dialog, settings copy.

---

## Phasing

```
Phase 0 This spec — contracts, term map, AWS-federation decision ✅ done (#590)
Phase 1 Agent record + uniform binding model + compat mapping (back-compat) ✅ done (#591, #592, + flag plumbing)
Phase 2 Bindable-primitives catalog API (Registry-lite, RBAC-composed) ← the palette (next)
Phase 3 Harness resolution: memory index injection + memory_* tools + model ← Workstream B payoff (thin slice, D6)
Phase 4 Agent Designer page (Agent Harness Editor) ← the headline UI, on P1–P2 contracts
Phase 5 Assistant deprecation + migration
Phase 2 Bindable-primitives catalog API (Registry-lite, RBAC-composed) ✅ done
Phase 3 Harness resolution: memory index injection + memory_* tools + model ✅ done
Phase 4 Agent Designer page (Agent Harness Editor) ✅ done
Phase 5 Assistant deprecation + migration 🔄 in progress (#746)
Later Federate AgentCore Registry / managed Harness as catalog+run backends (D1)
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ <h2 class="text-sm/6 font-semibold text-gray-900 dark:text-white">Persona</h2>
<!-- Name -->
<div class="flex-1">
<label for="name" class="block text-sm/6 font-medium text-gray-700 dark:text-gray-300">Name</label>
<input id="name" type="text" formControlName="name" placeholder="e.g. Research Assistant" class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" />
<input id="name" type="text" formControlName="name" placeholder="e.g. Research Agent" class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder:text-gray-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white" />
@if (getFieldError('name'); as e) { <p class="mt-1 text-xs/5 text-red-600 dark:text-red-400">{{ e }}</p> }
</div>
</div>
Expand Down Expand Up @@ -365,7 +365,7 @@ <h2 class="text-sm/6 font-semibold text-gray-900 dark:text-white">Memory spaces<
</section>
}

<!-- Knowledge base (shared component — also used by the assistant editor) -->
<!-- Knowledge base -->
<app-knowledge-base-section
[entityId]="agentId()"
[userPermission]="userPermission()"
Expand Down
79 changes: 79 additions & 0 deletions frontend/ai.client/src/app/app.routes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { provideRouter, Router, Routes } from '@angular/router';
import { provideLocationMocks } from '@angular/common/testing';
import { routes } from './app.routes';

/**
* Assistant-deprecation redirects (Designer Phase 5, #746).
*
* `/assistants*` no longer renders anything — it redirects onto the Agent surface. These
* paths are in people's bookmarks, in the "edit" link of every old chat session, and in
* links colleagues shared with each other, so the redirect is the compatibility promise:
* the ids are the same record on both sides (the compat mapping renders a legacy
* Assistant *as* an Agent — nothing was migrated), so the redirect lands on the same
* thing the old URL opened.
*
* Asserted against the real route table rather than a hand-built one: the bug this guards
* against is someone deleting the redirect entries, and a fixture table would not notice.
*/
describe('app routes — assistant deprecation redirects', () => {
@Component({ template: '' })
class BlankComponent {}

/**
* The real table, with every lazy `loadComponent` swapped for a blank component.
*
* Navigation must actually resolve for the router to report a final URL, and resolving
* the real pages would drag in their whole dependency graphs. The **paths** and
* `redirectTo` entries — the only thing under test — are preserved exactly.
*/
function stubbedRoutes(source: Routes): Routes {
return source.map((route) => {
const { loadComponent, loadChildren, children, canActivate, ...rest } = route;
const stubbed: Routes[number] = { ...rest };
if (children) stubbed.children = stubbedRoutes(children);
if ((loadComponent || loadChildren) && !rest.redirectTo) stubbed.component = BlankComponent;
return stubbed;
});
}

let router: Router;

beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
providers: [provideRouter(stubbedRoutes(routes)), provideLocationMocks()],
});
router = TestBed.inject(Router);
});

afterEach(() => {
TestBed.resetTestingModule();
});

it('sends the assistants list to the agents hub', async () => {
await router.navigateByUrl('/assistants');
expect(router.url).toBe('/agents');
});

it('sends the new-assistant form to the Designer', async () => {
await router.navigateByUrl('/assistants/new');
expect(router.url).toBe('/agents/new');
});

it('sends a bookmarked assistant editor to the same record in the Designer', async () => {
// The id must survive the redirect — that is the whole compatibility promise.
await router.navigateByUrl('/assistants/ast-001/edit');
expect(router.url).toBe('/agents/ast-001/edit');
});

it('does not swallow the agents routes it redirects onto', async () => {
await router.navigateByUrl('/agents/discover');
expect(router.url).toBe('/agents/discover');

await router.navigateByUrl('/agents/ast-001');
expect(router.url).toBe('/agents/ast-001');
});
});
24 changes: 18 additions & 6 deletions frontend/ai.client/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,32 @@ export const routes: Routes = [
canActivate: [adminGuard],
loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
},
// ── Assistant deprecation (Designer Phase 5) ────────────────────────────────────
// There is one noun, and it is Agent (Marketplace D1). The Designer reached parity
// and then passed it — bindings, icons, listings, pins, `@`-mention and reports all
// exist only on the Agent surface — so the old editor had strictly less to offer for
// the same record.
//
// These stay as **redirects rather than deletions**: `/assistants/:id/edit` is in
// people's bookmarks, in old chat sessions' "edit" links and in links colleagues have
// shared with each other. The ids are identical on both sides (the compat mapping
// renders a legacy Assistant *as* an Agent — there was no data migration), so the
// redirect lands on the same record. Removing them would turn every one of those into
// a 404 for no gain.
{
path: 'assistants/new',
loadComponent: () => import('./assistants/assistant-form/assistant-form.page').then(m => m.AssistantFormPage),
canActivate: [authGuard],
redirectTo: 'agents/new',
pathMatch: 'full',
},
{
path: 'assistants/:id/edit',
loadComponent: () => import('./assistants/assistant-form/assistant-form.page').then(m => m.AssistantFormPage),
canActivate: [authGuard],
redirectTo: 'agents/:id/edit',
pathMatch: 'full',
},
{
path: 'assistants',
loadComponent: () => import('./assistants/assistants.page').then(m => m.AssistantsPage),
canActivate: [authGuard],
redirectTo: 'agents',
pathMatch: 'full',
},
{
path: 'agents/new',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export type ShareAssistantDialogResult = {
id="dialog-title"
class="text-base/7 font-semibold text-gray-900 dark:text-white"
>
Share assistant
Share agent
</h3>
<p
id="dialog-description"
Expand All @@ -128,7 +128,7 @@ export type ShareAssistantDialogResult = {
<!-- Public Assistant: Show shareable URL -->
<section class="space-y-3">
<p class="text-sm/6 text-gray-600 dark:text-gray-400">
This assistant is public and discoverable by everyone. Share this URL to let
This agent is public and discoverable by everyone. Share this URL to let
others start a conversation with it.
</p>
<div class="flex gap-2">
Expand Down
29 changes: 11 additions & 18 deletions frontend/ai.client/src/app/components/sidenav/sidenav.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,14 @@
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">New Session</span>
</button>

<!-- Assistants -->
<a
routerLink="/assistants"
routerLinkActive="bg-gray-200/80 dark:bg-white/10"
(click)="sidenavService.close()"
class="group flex w-full cursor-pointer items-center gap-3 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-gray-200/60 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary-500 dark:hover:bg-white/5">
<div class="flex size-7 shrink-0 items-center justify-center text-gray-500 dark:text-gray-400">
<svg class="size-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0 1 12 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 0 1-.673-.38m0 0A2.18 2.18 0 0 1 3 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 0 1 3.413-.387m7.5 0V5.25A2.25 2.25 0 0 0 13.5 3h-3a2.25 2.25 0 0 0-2.25 2.25v.894m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Assistants</span>
</a>

<!--
Agents (Agent Designer + Marketplace).
Agents (Agent Designer + Marketplace) — the ONE noun (Marketplace D1).

The "Assistants" entry that used to sit above this was removed in Designer
Phase 5. `/assistants*` now redirects here; the ids are the same record, so
nothing was migrated and no link broke. The Preview badge came off with it —
it existed only to say which of two competing nouns was still moving, and
there is no longer a second one.

Gated on the API surface alone (D14): `showAgents()` is "the /agents list call did
not 404", i.e. the AGENTS_API_ENABLED / AGENT_MARKETPLACE_ENABLED kill switches.
Expand All @@ -59,8 +51,10 @@
roles UI (see `AppRoleService.resolve_user_permissions`), which is what made the
`skills` and `scheduled-runs` gates inoperable.

The Preview badge stays until Assistant deprecation (#746) lands — until then the
sidenav ships both nouns, and the badge is what says which one is still moving.
⚠️ This is now the only authoring entry point, so `AGENTS_API_ENABLED=false` no
longer degrades to an older surface — it removes agent authoring outright. The
flag's semantics changed from "fall back to Assistants" to "there is nothing
here"; see `agents_enabled()` in the backend for the same note.
-->
@if (showAgents()) {
<a
Expand All @@ -74,7 +68,6 @@
</svg>
</div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Agents</span>
<span class="ml-auto rounded-full bg-amber-100 px-1.5 py-px text-[10px] font-semibold uppercase leading-4 tracking-wide text-amber-700 dark:bg-amber-400/15 dark:text-amber-300">Preview</span>
</a>
}

Expand Down
15 changes: 15 additions & 0 deletions frontend/ai.client/src/app/components/sidenav/sidenav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,19 @@ describe('Sidenav — Agents nav entry gating (D14)', () => {
const fixture = await renderSidenav();
expect(agentsNavLink(fixture)).toBeUndefined();
});

// ── Assistant deprecation (Designer Phase 5, #746) ────────────────────────────────
it('ships one noun: there is no Assistants entry beside Agents', async () => {
const fixture = await renderSidenav();
const html = fixture.nativeElement as HTMLElement;

expect(html.querySelector('a[href="/assistants"]')).toBeNull();
expect(html.textContent).not.toContain('Assistants');
expect(agentsNavLink(fixture)).toBeDefined();
});

it('drops the Preview badge, which only existed to disambiguate two nouns', async () => {
const fixture = await renderSidenav();
expect(agentsNavLink(fixture)!.textContent).not.toContain('Preview');
});
});
4 changes: 2 additions & 2 deletions frontend/ai.client/src/app/components/sidenav/sidenav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ export class Sidenav {
this.router.navigate(['']);
}

navigateToAssistants() {
navigateToAgents() {
this.sidenavService.close();
this.router.navigate(['/assistants']);
this.router.navigate(['/agents']);
}

toggleCollapse() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
(click)="toggleMenu()"
class="assistant-pill"
[class.open]="menuOpen()"
[attr.aria-label]="'Assistant: ' + name() + '. Click for options.'"
[attr.aria-label]="'Agent: ' + name() + '. Click for options.'"
[attr.aria-expanded]="menuOpen()"
aria-haspopup="menu"
>
Expand All @@ -59,7 +59,7 @@ import {
type="button"
(click)="toggleMenu()"
class="assistant-indicator"
[attr.aria-label]="'Assistant: ' + name() + '. Click for options.'"
[attr.aria-label]="'Agent: ' + name() + '. Click for options.'"
[attr.aria-expanded]="menuOpen()"
aria-haspopup="menu"
>
Expand Down Expand Up @@ -96,7 +96,7 @@ import {
class="indicator-menu"
[class.placement-down]="menuPlacement() === 'down'"
role="menu"
aria-label="Assistant actions"
aria-label="Agent actions"
>
<button
type="button"
Expand All @@ -116,7 +116,7 @@ import {
(click)="onEdit()"
>
<ng-icon name="heroPencilSquare" class="menu-icon" />
<span>Edit assistant</span>
<span>Edit agent</span>
</button>

<button
Expand Down
9 changes: 7 additions & 2 deletions frontend/ai.client/src/app/session/session.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,12 +857,17 @@ export class ConversationPage implements OnDestroy {
}

/**
* Navigate to the assistant edit page.
* Navigate to the Designer for the Agent driving this session.
*
* The id is the same record either way — the compat mapping renders a legacy Assistant
* *as* an Agent, so `/agents/:id/edit` opens what `/assistants/:id/edit` used to. Routed
* directly rather than through the redirect so the address bar never shows the retired
* path.
*/
editAssistant(): void {
const assistantId = this.assistant()?.assistantId;
if (assistantId) {
this.router.navigate(['/assistants', assistantId, 'edit']);
this.router.navigate(['/agents', assistantId, 'edit']);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ import { LocalSettingsService } from '../../../services/local-settings.service';
<div>
<span class="text-sm/6 font-medium text-gray-900 dark:text-white">Memories</span>
<p class="text-sm/6 text-gray-500 dark:text-gray-400">
View and manage what the assistant remembers about you.
View and manage what the agent remembers about you.
</p>
</div>
</div>
Expand Down