Skip to content

[ZEPPELIN-6741] Define shared AI Assistant UI contracts and message history behavior - #5561

Open
voidmatcha wants to merge 10 commits into
apache:masterfrom
voidmatcha:voidmatcha/assistant-v2-3-contracts
Open

voidmatcha wants to merge 10 commits into
apache:masterfrom
voidmatcha:voidmatcha/assistant-v2-3-contracts

Conversation

@voidmatcha

@voidmatcha voidmatcha commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

What is this PR for?

Define the shared types and message history behavior used by the Assistant UI and its host. This gives subsequent UI work a common contract to build against.

Depends on #5558.

What type of PR is it?

Improvement

Todos

What is the Jira issue?

ZEPPELIN-6741

How should this be tested?

Run from zeppelin-web-angular/:

npm run build-project:sdk
npm run typecheck:react
npm --prefix projects/zeppelin-react test -- src/entities/assistant/model/messageHistory.spec.ts

Screenshots (if appropriate)

N/A

Questions:

  • Does the license files need to update? No
  • Is there breaking changes for older versions? No
  • Does this needs documentation? No

@voidmatcha
voidmatcha changed the base branch from temp/voidmatcha/assistant-prerequisites to branch-voidmatcha-assistant-prerequisites-temp October 9, 2026 01:35
@voidmatcha voidmatcha closed this Oct 9, 2026
@voidmatcha voidmatcha reopened this Oct 9, 2026
@voidmatcha

voidmatcha commented Oct 9, 2026 •

Copy link
Copy Markdown
Member Author

This PR defines the types shared by the Assistant UI and its host, and adds the conversion from stored message history to visible UI messages. The current diff is +211/-0 across five files. Excluding 63 lines in *.spec.* tests, the review diff is +148/-0. There is no lockfile change.

This depends on #5558. Review and approval can happen now, but I will wait for #5558 to merge, then rebase this branch onto the latest master and retarget the PR to master before merging it.

@voidmatcha
voidmatcha force-pushed the branch-voidmatcha-assistant-prerequisites-temp branch from 4235999 to 95bd1b7 Compare October 10, 2026 13:40
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from 89a0192 to 80efed9 Compare October 10, 2026 13:40
@voidmatcha voidmatcha changed the title [ZEPPELIN-6741] Define shared Assistant UI contracts and message history behavior [ZEPPELIN-6741] Define shared AI Assistant UI contracts and message history behavior Oct 10, 2026
@voidmatcha
voidmatcha force-pushed the branch-voidmatcha-assistant-prerequisites-temp branch from 95bd1b7 to 397a5df Compare October 11, 2026 11:30
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from 699239b to 3e95adf Compare October 11, 2026 11:30
@voidmatcha
voidmatcha changed the base branch from branch-voidmatcha-assistant-prerequisites-temp to master October 15, 2026 14:03
Give the sidebar shell and transport one SDK type to consume without duplicating its declaration in their separate PRs.

Confidence: high
Scope-risk: requires dependent PR branches to synchronize
Not-tested: standalone contracts build in this temporary worktree
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from 3e95adf to 51fac8d Compare October 15, 2026 14:09
@voidmatcha

Copy link
Copy Markdown
Member Author

Rebased onto master after #5558 merged to remove duplicated prerequisite changes. This PR now contains only its five intended files. The relevant local checks passed.

{ id: 'u2', role: 'user', content: 'Another question' },
{ id: 'a2', role: 'assistant', content: 'Another answer' }
];
expect(joinPages(earlier, later)).toEqual([...earlier, ...later]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most common page boundary is a question at the end of the earlier page and its answer at the start of the later one, but no test covers it. All tests still pass after removing last?.role === 'assistant' && from joinPages, which would merge the question into the answer. How about adding this case?

Suggested change
expect(joinPages(earlier, later)).toEqual([...earlier, ...later]);
expect(joinPages(earlier, later)).toEqual([...earlier, ...later]);
expect(joinPages([later[0]], [later[1]])).toEqual(later);

Comment on lines +27 to +28
/** From history: the tools the server called before this answer. Live runs report them as events instead. */
toolCalls?: AssistantToolCallRef[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When one answer spans several rounds (text, tool, text, tool), this shape merges them into a single content and a single toolCalls, so the order between text and tools is lost. The server history keeps that order, and the Direction in the frontend proposal (a renderer per tool inside the action log, the approval flow) may later need the answer shown step by step. Changing it then would change the shape of this contract, and the transport and UI along with it. How about keeping each round as segments instead of merging them, and joining them in the current UI? If merging is intended, could you share why?

Comment thread zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts Outdated
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from f114b93 to 6e44f7b Compare October 15, 2026 14:30
@voidmatcha

Copy link
Copy Markdown
Member Author

Thanks for the review. I updated the history model to preserve Assistant rounds as ordered segments, matching how the server stores them. I also removed the name field from tool_call.done, which the server does not send, and added tests for a question/answer split across page boundaries and overlapping pages. The focused unit tests, TypeScript check, and ESLint passed locally. The follow-up transport PR #5563 has been updated to use the same contract.

Comment on lines +33 to +37
/** Current UI projections; render segments to show text and actions in their original order. */
content: string;
toolCalls?: AssistantToolCallRef[];
/** Ordered assistant rounds as stored by the server, including tool-only rounds. */
segments: AssistantMessageSegment[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content and toolCalls can be derived from segments, so storing them as well lets them drift apart, as the input of the overlapping page test already does. How about dropping both fields, keeping only the rounds, and deriving them where needed?

export interface AssistantRound { id: string; content: string; toolCalls: AssistantToolCallRef[] }
export interface AssistantAnswerMessage { id: string; role: 'assistant'; rounds: AssistantRound[] }
export const answerText = (answer: AssistantAnswerMessage) =>
  answer.rounds.map(round => round.content).filter(Boolean).join('\n\n');

Comment on lines +15 to +21
title?: string;
/** Account that created the conversation, shown as "Started by …". */
ownerId?: string;
/** Whether the requesting user can send messages to the conversation, as the server decides. */
canSendMessage?: boolean;
/** Last activity (ISO time), shown in the conversation list. */
updatedAt?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these fields need to be optional?

export interface AssistantMessagePage {
messages: AssistantMessage[];
/** Pass as `before` to load the page before this one; null when this page starts the conversation. */
earlierCursor: string | null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be cursor?

See MessagesResponse.java.

earlierCursor: string | null;
}
export interface AssistantRunBody {
prompt: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server currently expects this field to be named content.

getMessages(conversationId: string, before?: string): Promise<AssistantMessagePage>;
/** Observe local run state, including a lost connection until history is reloaded. */
subscribeRunState?(conversationId: string, listener: (state: AssistantRunState) => void): () => void;
openRun(conversationId: string, body: AssistantRunBody, signal: AbortSignal): AsyncIterable<AssistantRunEvent>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like cancellation isn't supported on the server side.

tbonelee
tbonelee previously approved these changes Oct 15, 2026
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from da1d64c to 290cae6 Compare October 15, 2026 15:43
content: string;
toolCalls?: AssistantToolCallRef[];
}
/** One page of a conversation's history, oldest first. */

@seung-00 seung-00 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server’s listMessages() returns messages newest first, but this comment says “oldest first.” Should these match?
For example, assuming m5 is the newest message, the server returns [m5, m4, m3].

@voidmatcha

Copy link
Copy Markdown
Member Author

@seung-00 Thank you for the thorough review. I removed the duplicated fields and aligned the request and response contracts with the server. #5563 converts the server’s newest-first messages to chronological order for the UI. The ordering and client-only abort behavior are documented and covered by tests.

Comment on lines +54 to +61
export type AssistantRunEvent =
| { type: 'run.started'; runId?: string }
| { type: 'run.completed'; runId?: string }
| { type: 'run.failed'; runId?: string; message: string }
| { type: 'message.delta'; messageId: string; delta: string }
| { type: 'message.done'; messageId: string; content: string }
| { type: 'tool_call.started'; toolCallId: string; name: string }
| { type: 'tool_call.done'; toolCallId: string };

@seung-00 seung-00 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for reference, the server wraps the event data in payload. See AssistantEventPayload

Also, run.failed provides an error status rather than a message field.
For example:

{
  "conversationId": "...",
  "type": "run.failed",
  "payload": {
    "runId": "...",
    "error": {
      "status": 500
    }
  }
}

We could add a code for more specific handling later if needed.

@voidmatcha voidmatcha Oct 15, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification. AssistantRunEvent is the UI-facing event, not the raw WebSocket event. The follow-up transport in #5563 reads payload.error.status and maps it to the message field; this is covered by its tests.

export const NOTEBOOK_SIDEBAR_WIDTH = { initial: 370, min: 280, max: 800 } as const;

/** Result of a host reveal: scrolled to it, already visible, or not rendered. */
export type AssistantRevealResult = 'shown' | 'visible' | 'missing';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this type be used?

@voidmatcha voidmatcha Oct 15, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is for the revealParagraph callback planned in ZEPPELIN-6749. The Angular host would report whether a linked paragraph was scrolled into view, was already visible, or could not be found, and the React panel would announce the missing case. Since it is not consumed in this PR, I removed the declaration here and will define the result type alongside that callback.

return {
...later,
segments
};

@seung-00 seung-00 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make the return value more explicit? Spreading later could ignore some of earlier’s fields, such as toolCalls.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful review. toolCalls live on each segment, so the existing merge retains the calls from both pages. I agree that the top-level merge policy should be explicit, so I replaced ...later with id, role, and segments. All five message-history tests pass.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants