Skip to content

Commit 11a3ff5

Browse files
committed
resolve comments
1 parent 56a1a32 commit 11a3ff5

7 files changed

Lines changed: 91 additions & 75 deletions

File tree

agent-service/src/server.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import type {
4040
ReActStep,
4141
} from "./types/agent";
4242
import { AgentState, OperatorResultSerializationMode } from "./types/agent";
43-
import type { WsClientRequest, WsServerMessage, WsServerSnapshotMessage, OperatorResultSummaryWs } from "./types/ws";
43+
import type { WsClientCommand, WsServerEvent, OperatorResultSummaryWs } from "./types/ws";
4444

4545
const agentStore = new Map<string, TexeraAgent>();
4646
let agentCounter = 0;
@@ -325,7 +325,6 @@ const agentsRouter = new Elysia({ prefix: "/agents" })
325325
headId: stepId,
326326
steps: allSteps,
327327
workflowContent,
328-
operatorResults: getOperatorResultSummaries(agent),
329328
});
330329

331330
return {
@@ -434,7 +433,7 @@ function getOperatorResultSummaries(agent: TexeraAgent): Record<string, Operator
434433
return results;
435434
}
436435

437-
function broadcastToAgent(agentId: string, message: WsServerMessage): void {
436+
function broadcastToAgent(agentId: string, message: WsServerEvent): void {
438437
const agent = agentStore.get(agentId);
439438
if (!agent) return;
440439

@@ -474,7 +473,7 @@ export function buildApp() {
474473

475474
agent.addWebsocket(ws);
476475

477-
const snapshotMessage: WsServerSnapshotMessage = {
476+
const snapshotMessage: WsServerEvent = {
478477
type: "snapshot",
479478
state: agent.getState(),
480479
steps: agent.getAllSteps(),
@@ -492,19 +491,17 @@ export function buildApp() {
492491
return;
493492
}
494493

495-
let msg: WsClientRequest;
494+
let msg: WsClientCommand;
496495
try {
497-
msg = typeof messageData === "string" ? JSON.parse(messageData) : (messageData as WsClientRequest);
496+
msg = typeof messageData === "string" ? JSON.parse(messageData) : (messageData as WsClientCommand);
498497
} catch {
499498
ws.send(JSON.stringify({ type: "error", error: "Invalid message format" }));
500499
return;
501500
}
502501

503-
if (msg.type === "command") {
504-
if (msg.commandType === "stop") {
505-
agent.stop();
506-
broadcastToAgent(agentId, { type: "status", state: AgentState.STOPPING });
507-
}
502+
if (msg.type === "stop") {
503+
agent.stop();
504+
broadcastToAgent(agentId, { type: "status", state: AgentState.STOPPING });
508505
return;
509506
}
510507

@@ -544,7 +541,13 @@ export function buildApp() {
544541
// from GENERATING after errors).
545542
broadcastToAgent(agentId, { type: "status", state: agent.getState() });
546543
}
544+
return;
547545
}
546+
547+
// Frames are parsed from untrusted JSON; reject unknown discriminators
548+
// explicitly instead of silently no-op'ing, so client/server mismatches
549+
// are easy to diagnose.
550+
ws.send(JSON.stringify({ type: "error", error: `Unknown message type: ${(msg as { type?: unknown }).type}` }));
548551
},
549552

550553
close(ws) {
@@ -603,7 +606,7 @@ function printStartupMessage(app: ReturnType<typeof buildApp>) {
603606
console.log(` WS ${route.path}`);
604607
}
605608
console.log(" Send: { type: 'prompt', content: '...' }");
606-
console.log(" Send: { type: 'command', commandType: 'stop' }");
609+
console.log(" Send: { type: 'stop' }");
607610
console.log(" Recv: { type: 'snapshot' | 'step' | 'status' | 'error' | 'headChange', ... }");
608611
}
609612

agent-service/src/server.ws.spec.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ describe(`WS ${API}/agents/:id/react`, () => {
168168
await waitOpen(ws);
169169
await messages.waitFor(m => m.type === "snapshot");
170170

171-
ws.send(JSON.stringify({ type: "command", commandType: "stop" }));
171+
ws.send(JSON.stringify({ type: "stop" }));
172172

173173
const status = await messages.waitFor(m => m.type === "status");
174174
expect(status.state).toBe("STOPPING");
@@ -198,6 +198,18 @@ describe(`WS ${API}/agents/:id/react`, () => {
198198
expect(err.error).toBe("Invalid message format");
199199
});
200200

201+
test("an unknown message type yields an error frame", async () => {
202+
const id = await createAgent();
203+
const { ws, messages } = connect(id);
204+
await waitOpen(ws);
205+
await messages.waitFor(m => m.type === "snapshot");
206+
207+
ws.send(JSON.stringify({ type: "bogus" }));
208+
209+
const err = await messages.waitFor(m => m.type === "error");
210+
expect(err.error).toBe("Unknown message type: bogus");
211+
});
212+
201213
test("a prompt run streams GENERATING -> step -> resting status (no result frames)", async () => {
202214
const id = await createAgent();
203215

agent-service/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* under the License.
1818
*/
1919

20+
export * from "./util";
2021
export * from "./workflow";
2122
export * from "./execution";
2223
export * from "./agent";

agent-service/src/types/util.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
// Generic type-level utilities shared across the type modules.
21+
22+
/**
23+
* Builds a discriminated union from a `{ type -> payload }` map: each entry
24+
* becomes `{ type: <key> } & <payload>`, then they are unioned. The `type` tag
25+
* is derived from the map key rather than hand-written into each payload
26+
*/
27+
export type CustomUnionType<T> = { [K in keyof T]: { readonly type: K } & T[K] }[keyof T];

agent-service/src/types/ws/client.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,25 @@
1818
*/
1919

2020
// Client -> server WebSocket frames for this service's protocol
21-
// (`/agents/:id/react`). Modeled as a discriminated union on `type`, so each
22-
// request kind carries only its own fields.
2321

24-
/** Shared discriminator base; every client request sets a unique `type`. */
25-
interface WsClientRequestBase {
26-
type: "prompt" | "command";
27-
}
22+
import type { CustomUnionType } from "../util";
2823

2924
/**
30-
* A user prompt for the agent to run. `messageSource` notes where it
31-
* originated (interactive chat vs. an operator feedback action).
25+
* Send a prompt to agent to start the ReAct loop
3226
*/
33-
export interface WsClientRequestPrompt extends WsClientRequestBase {
34-
type: "prompt";
27+
export interface WsClientPromptCommand extends Readonly<{
3528
content: string;
3629
messageSource?: "chat" | "feedback";
37-
}
30+
}> {}
3831

3932
/**
40-
* A control command. Today the only command stops the in-flight run; the
41-
* `commandType` discriminator leaves room for more commands later.
33+
* Stop the agent's current ReAct loop
4234
*/
43-
export interface WsClientRequestStopCommand extends WsClientRequestBase {
44-
type: "command";
45-
commandType: "stop";
46-
}
35+
export interface WsClientStopCommand extends Readonly<{}> {}
4736

48-
/** Discriminated union of every client -> server frame. */
49-
export type WsClientRequest = WsClientRequestPrompt | WsClientRequestStopCommand;
37+
export type WsClientCommandTypeMap = {
38+
prompt: WsClientPromptCommand;
39+
stop: WsClientStopCommand;
40+
};
41+
42+
export type WsClientCommand = CustomUnionType<WsClientCommandTypeMap>;

agent-service/src/types/ws/server.ts

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@
1818
*/
1919

2020
// Server -> client WebSocket frames for this service's protocol
21-
// (`/agents/:id/react`). Modeled as a discriminated union on `type`, so each
22-
// message kind declares exactly the fields it sends.
2321

2422
import type { AgentState, ReActStep } from "../agent";
2523
import type { WorkflowContent } from "../workflow";
24+
import type { CustomUnionType } from "../util";
2625

2726
/**
2827
* Wire projection of one operator's execution result, summarized for the
@@ -42,69 +41,54 @@ export interface OperatorResultSummaryWs {
4241
resultStatistics?: Record<string, string>;
4342
}
4443

45-
/** Per-operator result summaries, keyed by operator id. */
46-
type OperatorResults = Record<string, OperatorResultSummaryWs>;
47-
48-
/** Shared discriminator base; every server frame sets a unique `type`. */
49-
interface WsServerMessageBase {
50-
type: "snapshot" | "step" | "status" | "error" | "headChange";
51-
}
52-
5344
/**
5445
* Full state pushed once when a client connects: the agent's current lifecycle
5546
* state, the complete step list, and the HEAD pointer. Operator results are not
5647
* included — they are pulled on demand via `GET /operator-results`.
5748
*/
58-
export interface WsServerSnapshotMessage extends WsServerMessageBase {
59-
type: "snapshot";
49+
export interface WsServerSnapshotEvent extends Readonly<{
6050
state: AgentState;
6151
steps: ReActStep[];
6252
headId: string;
63-
}
53+
}> {}
6454

65-
/**
66-
* A single ReAct step, streamed live as the agent runs.
67-
*/
68-
export interface WsServerStepMessage extends WsServerMessageBase {
69-
type: "step";
55+
/** A single ReAct step, streamed live as the agent runs. */
56+
export interface WsServerStepEvent extends Readonly<{
7057
step: ReActStep;
71-
}
58+
}> {}
7259

7360
/**
7461
* An agent lifecycle transition (e.g. GENERATING when a run starts, the resting
7562
* state when it ends, STOPPING on stop).
7663
*/
77-
export interface WsServerStatusMessage extends WsServerMessageBase {
78-
type: "status";
64+
export interface WsServerStatusEvent extends Readonly<{
7965
state: AgentState;
80-
}
66+
}> {}
8167

8268
/** An error surfaced to the client (agent not found, bad request, failed run). */
83-
export interface WsServerErrorMessage extends WsServerMessageBase {
84-
type: "error";
69+
export interface WsServerErrorEvent extends Readonly<{
8570
error: string;
86-
}
71+
}> {}
8772

8873
/**
8974
* Emitted after a checkout: HEAD moved, carrying the full step list and the
9075
* workflow snapshot at the new head.
9176
*
92-
* @deprecated Redundant and unused — the checkout flow that produces this frame
93-
* is unreachable in the product (nothing invokes the client's `checkoutStep()`).
94-
* Scheduled for removal (see #5930); do not build new code on it.
77+
* @deprecated Redundant and unused. TODO: remove this message and related caller logics.
9578
*/
96-
export interface WsServerHeadChangeMessage extends WsServerMessageBase {
97-
type: "headChange";
79+
export interface WsServerHeadChangeEvent extends Readonly<{
9880
headId: string;
9981
steps: ReActStep[];
10082
workflowContent?: WorkflowContent;
101-
operatorResults: OperatorResults;
102-
}
83+
}> {}
84+
85+
export type WsServerEventTypeMap = {
86+
snapshot: WsServerSnapshotEvent;
87+
step: WsServerStepEvent;
88+
status: WsServerStatusEvent;
89+
error: WsServerErrorEvent;
90+
headChange: WsServerHeadChangeEvent;
91+
};
10392

10493
/** Discriminated union of every server -> client frame. */
105-
export type WsServerMessage =
106-
| WsServerSnapshotMessage
107-
| WsServerStepMessage
108-
| WsServerStatusMessage
109-
| WsServerErrorMessage
110-
| WsServerHeadChangeMessage;
94+
export type WsServerEvent = CustomUnionType<WsServerEventTypeMap>;

frontend/src/app/workspace/service/agent/agent.service.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -537,10 +537,6 @@ export class AgentService {
537537
};
538538
tracking.workflowSubject.next(workflow as Workflow);
539539
}
540-
// Update operator results on HEAD change
541-
if (message.operatorResults) {
542-
this.updateOperatorResultSummaries(message.operatorResults);
543-
}
544540
break;
545541

546542
case "error":
@@ -952,7 +948,7 @@ export class AgentService {
952948
if (tracking?.websocket && tracking.websocket.readyState === WebSocket.OPEN) {
953949
// Send stop via WebSocket for immediate effect
954950
try {
955-
tracking.websocket.send(JSON.stringify({ type: "command", commandType: "stop" }));
951+
tracking.websocket.send(JSON.stringify({ type: "stop" }));
956952
} catch (error) {
957953
console.error("Failed to send stop command:", error);
958954
}

0 commit comments

Comments
 (0)