Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
7a57927
docs: subagent support design spec
mme Jul 7, 2026
431f3b1
docs: subagent support implementation plan
mme Jul 7, 2026
1bb7368
feat(python): add optional subagent_id to message types
mme Jul 7, 2026
2bcccbb
feat(core): add optional subagentId to message schemas
mme Jul 7, 2026
36e74f7
feat(core): add optional subagentId to creation/standalone events
mme Jul 7, 2026
343a03d
feat(core): add SUBAGENT_STARTED/FINISHED/ERROR lifecycle events
mme Jul 7, 2026
2d97dab
feat(python): add subagent_id to events and SUBAGENT_* lifecycle events
mme Jul 7, 2026
70b7619
feat(client): add subagent lifecycle subscriber hooks
mme Jul 7, 2026
1ce794c
feat(client): propagate subagentId through chunk transform
mme Jul 7, 2026
236936f
feat(client): enforce subagent lifecycle consistency in verifier
mme Jul 7, 2026
1408a2a
feat(client): dispatch subagent lifecycle subscriber hooks
mme Jul 7, 2026
ad9a85e
test(client): strengthen subagent tests + drop dead field (CR round 1)
mme Jul 7, 2026
be5cd8b
chore: format and lint fixes for subagent support
mme Jul 7, 2026
4e80c92
feat: add optional subagentId to REASONING_MESSAGE_CHUNK
mme Jul 7, 2026
81ce523
test: make subagent attribution assertions non-vacuous
mme Jul 7, 2026
6212b08
test(python): guard remaining subagent_id event assertions via by-ali…
mme Jul 7, 2026
36b0a27
docs: plan for subagent backward-compatibility shim
mme Jul 7, 2026
c2a3c27
feat(client): add version-gated subagent backward-compat shim (0.0.57)
mme Jul 7, 2026
4370f8c
test(client): e2e verify 0.0.57 shim strips subagents through run pip…
mme Jul 7, 2026
5c8eeab
docs: correct subagent Phase 2 backward-compat rationale
mme Jul 7, 2026
f1c6b0b
fix(client): strip subagentId from RUN_STARTED.input echo + harden 0.…
mme Jul 7, 2026
b71bac1
fix(client): make 0.0.57 shim warn-test env-independent + correct docs
mme Jul 7, 2026
703bb7e
docs: deepagents subagent attribution demo (step 1) design
mme Jul 7, 2026
31ac667
docs: deepagents subagent demo implementation plan
mme Jul 7, 2026
c4f451a
docs: fold spike findings into deepagents demo plan (detection signal…
mme Jul 7, 2026
3caf759
chore(langgraph): bump langchain/langgraph to deepagents-compatible v…
mme Jul 7, 2026
72b79c3
spike(langgraph): document deepagents subagent event surfacing
mme Jul 7, 2026
3fcd6c2
feat(langgraph): derive deepagents subagent context from nested check…
mme Jul 7, 2026
1c4ce10
feat(langgraph): emit SUBAGENT_STARTED/FINISHED/ERROR at subagent bou…
mme Jul 7, 2026
9c078ce
fix(python-sdk): export SUBAGENT_STARTED/FINISHED/ERROR events from a…
mme Jul 7, 2026
0a4f562
feat(langgraph): stamp subagentId on creation events while in a subagent
mme Jul 7, 2026
4065217
feat(langgraph-examples): add deepagents 3-subagent attribution demo …
mme Jul 7, 2026
51842f7
fix(langgraph): emit one SUBAGENT_STARTED/FINISHED per subagent under…
mme Jul 7, 2026
58a88cb
feat(dojo): add deepagents subagent-attribution demo page + wiring
mme Jul 7, 2026
074bf3d
fix(dojo): scope deepagents_subagents demo to the python-backed integ…
mme Jul 7, 2026
37c6449
chore: override CopilotKit's @ag-ui deps to workspace so subagentId r…
mme Jul 7, 2026
4727cf4
chore(langgraph-examples): temporarily reduce deepagents demo to one …
mme Jul 7, 2026
bddd9ba
fix(langgraph): include subagent messages (with subagentId) in MESSAG…
mme Jul 7, 2026
53f9102
feat(langgraph): subagent tool-call snapshot + name/description; inli…
mme Jul 8, 2026
a36c69b
chore(temp): flip integration @ag-ui deps to workspace:* for pkg.pr.n…
mme Jul 8, 2026
557ca11
Revert workspace:* flip — breaks pkg-pr-new pack (pnpm pack cannot re…
mme Jul 8, 2026
39a4865
docs: update BEFORE-MERGE (pkg.pr.new url path abandoned; use @ag-ui …
mme Jul 8, 2026
e503a7a
chore: undo pkg.pr.new preview plumbing
mme Jul 8, 2026
35fc222
feat(dojo): subagent attribution UI — name via useSubagent + tool-cal…
mme Jul 8, 2026
b6feeb1
feat(dojo): group subagent messages into one reasoning-style collapsible
mme Jul 8, 2026
fed8fee
feat(subagents): finish subagent on task-end + reasoning-style collap…
mme Jul 8, 2026
3738b68
refactor(dojo): render subagent groups via renderCustomMessages + mul…
mme Jul 9, 2026
b77cb96
demo(deepagents): run 3 subagents in parallel and restore tool-call a…
mme Jul 9, 2026
3d55805
feat(dojo): show a subtle checkmark when a subagent finishes
mme Jul 9, 2026
acec0b6
fix(dojo): collapse subagent group immediately, animate only on open
mme Jul 9, 2026
8d8dd47
refactor(dojo): own the subagent header as a standalone component
mme Jul 9, 2026
a7d6c92
fix(langgraph): drop subagent-attributed messages from run input
mme Jul 9, 2026
bdfd42b
feat(langgraph): persist subagent messages across turns via the snapshot
mme Jul 9, 2026
85ff694
chore: remove superpowers plans and specs from the branch
mme Jul 14, 2026
c69e3bd
chore: remove subagent spike/investigation notes from the demo dir
mme Jul 14, 2026
4f6e141
fix(subagents): terminal events on error + fix failing test (CR round)
mme Jul 14, 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
Prev Previous commit
Next Next commit
feat(client): add version-gated subagent backward-compat shim (0.0.57)
BackwardCompatibility_0_0_57 strips all subagent additions when talking to a
pre-subagent (<=0.0.57) remote agent: removes subagentId from input messages on
the way in, and drops SUBAGENT_STARTED/FINISHED/ERROR events + strips subagentId
from surviving events and MESSAGES_SNAPSHOT messages on the way out. Registered
behind compareVersions(maxVersion, '0.0.57') <= 0, and client bumped to 0.0.58 so
a default modern client keeps its own subagent data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • Loading branch information
mme and claude committed Jul 7, 2026
commit c2a3c27c75d2022c8814b18ef8ab8c2630a99e93
2 changes: 1 addition & 1 deletion sdks/typescript/packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ag-ui/client",
"author": "Markus Ecker <markus.ecker@gmail.com>",
"version": "0.0.57",
"version": "0.0.58",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
6 changes: 6 additions & 0 deletions sdks/typescript/packages/client/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
BackwardCompatibility_0_0_39,
BackwardCompatibility_0_0_45,
BackwardCompatibility_0_0_47,
BackwardCompatibility_0_0_57,
} from "@/middleware";
import packageJson from "../../package.json";

Expand Down Expand Up @@ -126,6 +127,11 @@ export abstract class AbstractAgent {
this.middlewares.unshift(new BackwardCompatibility_0_0_47());
}

// Auto-insert BackwardCompatibility_0_0_57 for backward compatibility with
// pre-subagent agents: strips subagentId and drops SUBAGENT_* lifecycle events.
if (compareVersions(this.maxVersion, "0.0.57") <= 0) {
this.middlewares.unshift(new BackwardCompatibility_0_0_57());
}
}

public subscribe(subscriber: AgentSubscriber) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, it, expect } from "vitest";
import { AbstractAgent } from "@/agent";
import { BaseEvent, EventType, RunAgentInput } from "@ag-ui/core";
import { Observable, from, lastValueFrom, toArray } from "rxjs";
import { BackwardCompatibility_0_0_57 } from "../backward-compatibility-0-0-57";

// Mock agent that records the input it received and replays a scripted stream.
class MockAgent extends AbstractAgent {
public lastInput?: RunAgentInput;
private events: BaseEvent[];

constructor(events: BaseEvent[] = []) {
super({});
this.events = events;
}

override get maxVersion(): string {
return "0.0.57";
}

override run(input: RunAgentInput): Observable<BaseEvent> {
this.lastInput = input;
return from(this.events);
}
}

const createInput = (overrides: Partial<RunAgentInput> = {}): RunAgentInput => ({
threadId: "thread-1",
runId: "run-1",
state: {},
messages: [],
tools: [],
context: [],
forwardedProps: {},
...overrides,
});

describe("BackwardCompatibility_0_0_57", () => {
it("strips subagentId from input messages before the agent sees them", async () => {
const middleware = new BackwardCompatibility_0_0_57();
const agent = new MockAgent([]);
const input = createInput({
messages: [
{ id: "m1", role: "assistant", content: "hi", subagentId: "sub-1" } as any,
{ id: "m2", role: "user", content: "yo" } as any,
],
});

await lastValueFrom(middleware.run(input, agent).pipe(toArray()));

expect((agent.lastInput!.messages[0] as any).subagentId).toBeUndefined();
expect(agent.lastInput!.messages[0].content).toBe("hi");
expect((agent.lastInput!.messages[1] as any).subagentId).toBeUndefined();
});

it("drops SUBAGENT_STARTED/FINISHED/ERROR events from the output stream", async () => {
const middleware = new BackwardCompatibility_0_0_57();
const events: BaseEvent[] = [
{ type: EventType.RUN_STARTED, threadId: "thread-1", runId: "run-1" } as any,
{ type: EventType.SUBAGENT_STARTED, subagentId: "s1", name: "R" } as any,
{ type: EventType.TEXT_MESSAGE_START, messageId: "m1", subagentId: "s1" } as any,
{ type: EventType.SUBAGENT_ERROR, subagentId: "s1", message: "x" } as any,
{ type: EventType.SUBAGENT_FINISHED, subagentId: "s1" } as any,
{ type: EventType.RUN_FINISHED, threadId: "thread-1", runId: "run-1" } as any,
];

const result = await lastValueFrom(
middleware.run(createInput(), new MockAgent(events)).pipe(toArray()),
);

const types = result.map((e) => e.type);
expect(types).toEqual([
EventType.RUN_STARTED,
EventType.TEXT_MESSAGE_START,
EventType.RUN_FINISHED,
]);
});

it("strips subagentId from surviving events", async () => {
const middleware = new BackwardCompatibility_0_0_57();
const events: BaseEvent[] = [
{ type: EventType.TEXT_MESSAGE_START, messageId: "m1", subagentId: "s1" } as any,
];

const result = await lastValueFrom(
middleware.run(createInput(), new MockAgent(events)).pipe(toArray()),
);

expect((result[0] as any).subagentId).toBeUndefined();
expect((result[0] as any).messageId).toBe("m1");
});

it("strips subagentId from messages inside MESSAGES_SNAPSHOT", async () => {
const middleware = new BackwardCompatibility_0_0_57();
const events: BaseEvent[] = [
{
type: EventType.MESSAGES_SNAPSHOT,
messages: [
{ id: "m1", role: "assistant", content: "hi", subagentId: "s1" },
{ id: "m2", role: "user", content: "yo" },
],
} as any,
];

const result = await lastValueFrom(
middleware.run(createInput(), new MockAgent(events)).pipe(toArray()),
);

const snapshot = result[0] as any;
expect(snapshot.messages[0].subagentId).toBeUndefined();
expect(snapshot.messages[0].content).toBe("hi");
expect(snapshot.messages[1].subagentId).toBeUndefined();
});

it("leaves a subagent-free stream and input untouched", async () => {
const middleware = new BackwardCompatibility_0_0_57();
const events: BaseEvent[] = [
{ type: EventType.RUN_STARTED, threadId: "thread-1", runId: "run-1" } as any,
{ type: EventType.TEXT_MESSAGE_START, messageId: "m1", role: "assistant" } as any,
{ type: EventType.RUN_FINISHED, threadId: "thread-1", runId: "run-1" } as any,
];
const agent = new MockAgent(events);
const input = createInput({ messages: [{ id: "m0", role: "user", content: "hi" } as any] });

const result = await lastValueFrom(middleware.run(input, agent).pipe(toArray()));

expect(result.map((e) => e.type)).toEqual([
EventType.RUN_STARTED,
EventType.TEXT_MESSAGE_START,
EventType.RUN_FINISHED,
]);
expect(agent.lastInput!.messages[0].content).toBe("hi");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Middleware } from "./middleware";
import { AbstractAgent } from "@/agent";
import type { RunAgentInput, BaseEvent } from "@ag-ui/core";
import { EventType } from "@ag-ui/core";
import type { Observable } from "rxjs";
import { filter, map } from "rxjs/operators";

// Subagent lifecycle event types (introduced after 0.0.57). Referenced as string
// literals so this shim keeps compiling even if the enum members are ever removed.
const SUBAGENT_STARTED = "SUBAGENT_STARTED";
const SUBAGENT_FINISHED = "SUBAGENT_FINISHED";
const SUBAGENT_ERROR = "SUBAGENT_ERROR";

/** Returns a shallow copy of `obj` with any `subagentId` key removed. */
function stripSubagentId<T extends object>(obj: T): T {
if (obj && typeof obj === "object" && "subagentId" in obj) {
const { subagentId: _subagentId, ...rest } = obj as T & { subagentId?: unknown };
return rest as T;
}
return obj;
}

/**
* Middleware that removes all subagent-support additions when talking to a
* pre-subagent (<= 0.0.57) agent:
* - input: strips `subagentId` from every message before the agent sees it.
* - output: drops SUBAGENT_STARTED/FINISHED/ERROR events entirely, and strips
* `subagentId` from every remaining event and from each message inside
* a MESSAGES_SNAPSHOT.
*
* The subagent feature is purely additive, so this shim is a pure removal in both
* directions; there is no field/event to translate (unlike 0.0.45's THINKING->REASONING).
*/
export class BackwardCompatibility_0_0_57 extends Middleware {
override run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
const sanitizedInput: RunAgentInput = {
...input,
messages: (input.messages ?? []).map((message) => stripSubagentId(message)),
} as RunAgentInput;

return this.runNext(sanitizedInput, next).pipe(
filter((event) => {
const type = event.type as string;
return type !== SUBAGENT_STARTED && type !== SUBAGENT_FINISHED && type !== SUBAGENT_ERROR;
}),
map((event) => {
const stripped = stripSubagentId(event);
if (stripped.type === EventType.MESSAGES_SNAPSHOT) {
const snapshot = stripped as BaseEvent & { messages?: Array<Record<string, unknown>> };
if (Array.isArray(snapshot.messages)) {
return {
...snapshot,
messages: snapshot.messages.map((message) => stripSubagentId(message)),
} as BaseEvent;
}
}
return stripped;
}),
);
}
}
1 change: 1 addition & 0 deletions sdks/typescript/packages/client/src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { FilterToolCallsMiddleware } from "./filter-tool-calls";
export { BackwardCompatibility_0_0_39 } from "./backward-compatibility-0-0-39";
export { BackwardCompatibility_0_0_45 } from "./backward-compatibility-0-0-45";
export { BackwardCompatibility_0_0_47 } from "./backward-compatibility-0-0-47";
export { BackwardCompatibility_0_0_57 } from "./backward-compatibility-0-0-57";