2025-08-01 09:38:48 +08:00
|
|
|
import message from '@/components/ui/message';
|
2025-06-06 16:30:18 +08:00
|
|
|
import { Authorization } from '@/constants/authorization';
|
feat(agent): align Go agent behavior with Python (except retrieval component) (#16225)
## Summary
Aligns the **Go agent runtime/canvas/components/tools** behavior with
the **Python `agent/` implementation** so the same stored canvas DSL
produces the same execution result on either side. Every component,
tool, and runtime primitive in `internal/agent/` is now driven by the
same semantics as its Python counterpart — variable resolution, template
substitution, control flow, error reporting, retry/cancel, and stream
event shapes.
The **retrieval component is the one explicit exception** in this PR. It
is being reworked in a separate change and is excluded from this
alignment pass; the wrapper slot (`universe_a_wrappers.go →
newRetrievalComponent`) is preserved.
## Scope of alignment
### Components (all aligned with `agent/component/`)
`Begin` · `Message` · `LLM` (incl. ChatTemplateKwargs,
MessageHistoryWindowSize, VisualFiles, Cite, OutputStructure,
JSONOutput, TopP, MaxRetries, DelayAfterError, credentials) · `Agent`
(react + tool artifact capture + `Reset()` interface-assert) · `Switch`
(12/12 operators, Python-equivalent semantics) · `Categorize` · `Invoke`
· `Iteration` · `Loop` (macro-expansion through `workflowx.AddLoopNode`)
· `UserFillUp` (Python-equivalent interrupt/resume via eino
`compose.Interrupt`/`ResumeWithData`) · `FillUp` · `DataOperations` ·
`ListOperations` · `StringTransform` · `VariableAggregator` ·
`VariableAssigner` · `Browser` (full stagehand runtime parity) ·
`DocsGenerator` · `ExcelProcessor`.
### Tools (all aligned with `agent/tools/`)
`Retrieval` (wrapper slot only — logic out of scope) · `MCPToolAdapter`
(streamable-HTTP) · `CodeExec` (sandbox bridge with
`code_exec_contract.go` matching Python contract) · `AkShare` · `ArXiv`
· `Crawler` · `DeepL` · `DuckDuckGo` · `Email` · `ExeSQL` · `GitHub` ·
`Google` · `GoogleScholar` · `Jin10` · `PubMed` · `QWeather` · `SearXNG`
· `Tavily` · `Tushare` · `Wencai` · `Wikipedia` · `YahooFinance` —
uniform `eino tool.InvokableTool` interface, SSRF protection, shared
HTTP client.
### Canvas execution engine (`internal/agent/canvas/`)
Aligned with Python's `agent/canvas.py`:
- **Scheduler** (`scheduler.go`): state pre/post handlers, node lambdas,
per-component timeout resolver (4-level: per-class env → per-class table
→ uniform env → 600s fallback), `legacyNoOpNames`.
- **Loop subgraph** (`loop_subgraph.go`): Python-equivalent
`AddLoopNode` macro expansion + condition translation.
- **Multibranch** (`multibranch.go`): `Switch` / `Categorize` routing
via `compose.NewGraphMultiBranch` — same branch selection semantics as
Python.
- **Parallel subgraph** (`parallel_subgraph.go`): matches Python's
parallel fan-out contract.
- **Interrupt/Resume** (`interrupt_resume.go`): `UserFillUpNodeBody` /
`IsInterruptError` / `ExtractInterruptContexts` — replaces the
deprecated Python sentinel chain with eino's native interrupt API,
preserving the same external behavior.
- **Checkpoint** (`checkpoint_store.go`): `RedisCheckPointStore`
Get/Set/Delete, with business metadata (status / canvas_id /
parent_run_id) on a parallel Redis Hash.
- **RunTracker** (`run_tracker.go`): Start / MarkSucceeded / MarkFailed
/ MarkCancelled / AttachCheckpoint — same lifecycle as the Python run
record.
- **Cancel** (`cancel.go`): Redis pub/sub watch.
- **Stream** (`stream.go`): SSE channel with `messages` / `waiting` /
`errors` / `done` events, same shape as Python's `agent.canvas.RunEvent`
payload.
### DSL bridge (`internal/agent/dsl/`)
- `normalize.go`: v1↔v2 collapsed into a single wire format — Python and
Go consume the same stored JSON.
- `reset.go`: per-run state reset matches Python's `Canvas.reset()`
semantics.
- Testdata mirrors Python's `agent_msg.json` / `all.json` / etc.
### Runtime (`internal/agent/runtime/`)
- `CanvasState` / `NewCanvasState` / `GetVar` / `SetVar` / `ReadVars`:
same `{{cpn_id@param}}` resolution model.
- `ResolveTemplate` (regex fast path + gonja fallback) — Python
Jinja-style semantics.
- `selector.go`, `metrics.go`, `component.go`: shared runtime contracts.
## Out of scope (intentionally)
- **`Retrieval` component logic** — wrapped only; full parity lands in a
follow-up PR.
- **Frontend** — only minor dsl-bridge / canvas UX fixes ride along.
- **CLI / admin / model registry** — orthogonal to agent behavior.
## How alignment is verified
`internal/service/agent_run_e2e_test.go` exercises the **full production
chain** against real Python-shaped DSL fixtures:
```
loadCanvasForUser → versionDAO.GetLatest → decodeCanvasFromDSL →
canvas.Compile → cc.Workflow.Invoke → answer extraction
```
using in-memory SQLite + miniredis (no Docker). Covers:
- `TestRunAgent_RealCanvas_BeginMessage` — happy path, `{{sys.query}}`
resolution
- `TestRunAgent_RealCanvas_WaitForUserResume` — two-run resume cycle
(Python-equivalent)
- `TestRunAgent_RealCanvas_CompileFails` — unknown component name →
sanitized error (Python-equivalent)
- `TestRunAgent_RealCanvas_InvokeFails` — unresolvable template ref
(Python-equivalent)
- `TestRunAgent_RunTracker_AttachCheckpoint_CallSequence` —
Start→AttachCheckpoint→MarkSucceeded lifecycle
`internal/handler/agent_test.go` — SSE streaming parity (`Content-Type:
text/event-stream`, `data: {…}\n\n`, trailing `data: [DONE]\n\n`,
OpenAI-compatible non-stream `choices`).
`internal/agent/canvas/fixture_compile_test.go` + per-component tests
pin the Python-equivalent outputs.
```
go test -count=1 -v -run 'TestRunAgent_RealCanvas|TestRunAgent_RunTracker' ./internal/service/
```
## Design reference
`docs/develop/agent-go-port-design.md` (1329 lines, last cross-checked
2026-06-17) — module layout, per-component / per-tool inventory,
corner-case catalogue, and the actionable backlog (Section 14, including
the retrieval alignment follow-up).
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 11:58:29 +08:00
|
|
|
import { ResponseType } from '@/interfaces/database/base';
|
2025-07-15 17:30:45 +08:00
|
|
|
import { IReferenceObject } from '@/interfaces/database/chat';
|
2025-06-30 10:36:52 +08:00
|
|
|
import { BeginQuery } from '@/pages/agent/interface';
|
2025-06-06 16:30:18 +08:00
|
|
|
import { getAuthorization } from '@/utils/authorization-util';
|
|
|
|
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
|
|
|
|
import { useCallback, useRef, useState } from 'react';
|
|
|
|
|
|
|
|
|
|
export enum MessageEventType {
|
|
|
|
|
WorkflowStarted = 'workflow_started',
|
|
|
|
|
NodeStarted = 'node_started',
|
|
|
|
|
NodeFinished = 'node_finished',
|
|
|
|
|
Message = 'message',
|
|
|
|
|
MessageEnd = 'message_end',
|
|
|
|
|
WorkflowFinished = 'workflow_finished',
|
2025-06-27 18:53:13 +08:00
|
|
|
UserInputs = 'user_inputs',
|
2026-06-26 22:55:49 +08:00
|
|
|
WaitingForUser = 'waiting_for_user',
|
2025-07-02 19:21:40 +08:00
|
|
|
NodeLogs = 'node_logs',
|
2025-06-06 16:30:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface IAnswerEvent<T> {
|
|
|
|
|
event: MessageEventType;
|
|
|
|
|
message_id: string;
|
2025-07-30 17:38:31 +08:00
|
|
|
session_id: string;
|
2025-06-06 16:30:18 +08:00
|
|
|
created_at: number;
|
|
|
|
|
task_id: string;
|
|
|
|
|
data: T;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface INodeData {
|
|
|
|
|
inputs: Record<string, any>;
|
|
|
|
|
outputs: Record<string, any>;
|
|
|
|
|
component_id: string;
|
2025-07-28 16:48:46 +08:00
|
|
|
component_name: string;
|
|
|
|
|
component_type: string;
|
2025-06-06 16:30:18 +08:00
|
|
|
error: null | string;
|
|
|
|
|
elapsed_time: number;
|
|
|
|
|
created_at: number;
|
2025-08-01 18:32:38 +08:00
|
|
|
thoughts: string;
|
2025-06-06 16:30:18 +08:00
|
|
|
}
|
|
|
|
|
|
2025-06-30 10:36:52 +08:00
|
|
|
export interface IInputData {
|
|
|
|
|
content: string;
|
|
|
|
|
inputs: Record<string, BeginQuery>;
|
|
|
|
|
tips: string;
|
|
|
|
|
}
|
2025-11-14 19:50:01 +08:00
|
|
|
export interface IAttachment {
|
|
|
|
|
doc_id: string;
|
|
|
|
|
format: string;
|
|
|
|
|
file_name: string;
|
|
|
|
|
}
|
2025-06-06 16:30:18 +08:00
|
|
|
export interface IMessageData {
|
|
|
|
|
content: string;
|
2025-12-02 19:39:43 +08:00
|
|
|
audio_binary: string;
|
2025-11-14 19:50:01 +08:00
|
|
|
outputs: any;
|
2025-07-17 12:10:03 +08:00
|
|
|
start_to_think?: boolean;
|
|
|
|
|
end_to_think?: boolean;
|
2025-06-06 16:30:18 +08:00
|
|
|
}
|
|
|
|
|
|
2025-07-15 17:30:45 +08:00
|
|
|
export interface IMessageEndData {
|
|
|
|
|
reference: IReferenceObject;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:21:40 +08:00
|
|
|
export interface ILogData extends INodeData {
|
|
|
|
|
logs: {
|
|
|
|
|
name: string;
|
|
|
|
|
result: string;
|
|
|
|
|
args: {
|
|
|
|
|
query: string;
|
|
|
|
|
topic: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:30:18 +08:00
|
|
|
export type INodeEvent = IAnswerEvent<INodeData>;
|
|
|
|
|
|
|
|
|
|
export type IMessageEvent = IAnswerEvent<IMessageData>;
|
|
|
|
|
|
2025-07-15 17:30:45 +08:00
|
|
|
export type IMessageEndEvent = IAnswerEvent<IMessageEndData>;
|
|
|
|
|
|
2025-06-30 10:36:52 +08:00
|
|
|
export type IInputEvent = IAnswerEvent<IInputData>;
|
|
|
|
|
|
2025-07-02 19:21:40 +08:00
|
|
|
export type ILogEvent = IAnswerEvent<ILogData>;
|
|
|
|
|
|
2025-07-15 17:30:45 +08:00
|
|
|
export type IChatEvent = INodeEvent | IMessageEvent | IMessageEndEvent;
|
2025-06-11 09:22:07 +08:00
|
|
|
|
|
|
|
|
export type IEventList = Array<IChatEvent>;
|
2025-06-06 16:30:18 +08:00
|
|
|
|
2026-06-26 22:55:49 +08:00
|
|
|
const parseAgentEventData = (data: any) => {
|
|
|
|
|
if (typeof data !== 'string') return data;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(data);
|
|
|
|
|
} catch {
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const normalizeAgentEvent = (value: any) => {
|
|
|
|
|
if (value?.event === MessageEventType.WaitingForUser) {
|
|
|
|
|
return {
|
|
|
|
|
...value,
|
|
|
|
|
event: MessageEventType.UserInputs,
|
|
|
|
|
data: parseAgentEventData(value.data),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-02 20:49:23 +08:00
|
|
|
export const useSendMessageBySSE = (url: string) => {
|
2025-06-06 16:30:18 +08:00
|
|
|
const [answerList, setAnswerList] = useState<IEventList>([]);
|
|
|
|
|
const [done, setDone] = useState(true);
|
|
|
|
|
const timer = useRef<any>();
|
|
|
|
|
const sseRef = useRef<AbortController>();
|
|
|
|
|
|
|
|
|
|
const initializeSseRef = useCallback(() => {
|
|
|
|
|
sseRef.current = new AbortController();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const resetAnswerList = useCallback(() => {
|
|
|
|
|
if (timer.current) {
|
|
|
|
|
clearTimeout(timer.current);
|
|
|
|
|
}
|
|
|
|
|
timer.current = setTimeout(() => {
|
|
|
|
|
setAnswerList([]);
|
|
|
|
|
clearTimeout(timer.current);
|
|
|
|
|
}, 1000);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const send = useCallback(
|
|
|
|
|
async (
|
|
|
|
|
body: any,
|
|
|
|
|
controller?: AbortController,
|
|
|
|
|
): Promise<{ response: Response; data: ResponseType } | undefined> => {
|
|
|
|
|
initializeSseRef();
|
|
|
|
|
try {
|
|
|
|
|
setDone(false);
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
[Authorization]: getAuthorization(),
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
signal: controller?.signal || sseRef.current?.signal,
|
|
|
|
|
});
|
2026-06-26 22:55:49 +08:00
|
|
|
// SSE streams (text/event-stream) emit `data: {...}\n\n` frames, not
|
|
|
|
|
// a single JSON document. The .clone().json() call below is kept
|
|
|
|
|
// for non-streaming callers (lastEventData will be set from the
|
|
|
|
|
// per-frame parser below when the response IS SSE); for SSE
|
|
|
|
|
// bodies the JSON parse rejects — swallow it silently instead
|
|
|
|
|
// of console.warn'ing `SyntaxError: Unexpected token 'd', "data:
|
|
|
|
|
// {"ev"... is not valid JSON` on every chat completion.
|
|
|
|
|
const responseDataPromise: Promise<ResponseType | undefined> = response
|
2026-06-24 11:51:24 +08:00
|
|
|
.clone()
|
|
|
|
|
.json()
|
|
|
|
|
.then((data: ResponseType) => data)
|
|
|
|
|
.catch(() => undefined);
|
feat(agent): align Go agent behavior with Python (except retrieval component) (#16225)
## Summary
Aligns the **Go agent runtime/canvas/components/tools** behavior with
the **Python `agent/` implementation** so the same stored canvas DSL
produces the same execution result on either side. Every component,
tool, and runtime primitive in `internal/agent/` is now driven by the
same semantics as its Python counterpart — variable resolution, template
substitution, control flow, error reporting, retry/cancel, and stream
event shapes.
The **retrieval component is the one explicit exception** in this PR. It
is being reworked in a separate change and is excluded from this
alignment pass; the wrapper slot (`universe_a_wrappers.go →
newRetrievalComponent`) is preserved.
## Scope of alignment
### Components (all aligned with `agent/component/`)
`Begin` · `Message` · `LLM` (incl. ChatTemplateKwargs,
MessageHistoryWindowSize, VisualFiles, Cite, OutputStructure,
JSONOutput, TopP, MaxRetries, DelayAfterError, credentials) · `Agent`
(react + tool artifact capture + `Reset()` interface-assert) · `Switch`
(12/12 operators, Python-equivalent semantics) · `Categorize` · `Invoke`
· `Iteration` · `Loop` (macro-expansion through `workflowx.AddLoopNode`)
· `UserFillUp` (Python-equivalent interrupt/resume via eino
`compose.Interrupt`/`ResumeWithData`) · `FillUp` · `DataOperations` ·
`ListOperations` · `StringTransform` · `VariableAggregator` ·
`VariableAssigner` · `Browser` (full stagehand runtime parity) ·
`DocsGenerator` · `ExcelProcessor`.
### Tools (all aligned with `agent/tools/`)
`Retrieval` (wrapper slot only — logic out of scope) · `MCPToolAdapter`
(streamable-HTTP) · `CodeExec` (sandbox bridge with
`code_exec_contract.go` matching Python contract) · `AkShare` · `ArXiv`
· `Crawler` · `DeepL` · `DuckDuckGo` · `Email` · `ExeSQL` · `GitHub` ·
`Google` · `GoogleScholar` · `Jin10` · `PubMed` · `QWeather` · `SearXNG`
· `Tavily` · `Tushare` · `Wencai` · `Wikipedia` · `YahooFinance` —
uniform `eino tool.InvokableTool` interface, SSRF protection, shared
HTTP client.
### Canvas execution engine (`internal/agent/canvas/`)
Aligned with Python's `agent/canvas.py`:
- **Scheduler** (`scheduler.go`): state pre/post handlers, node lambdas,
per-component timeout resolver (4-level: per-class env → per-class table
→ uniform env → 600s fallback), `legacyNoOpNames`.
- **Loop subgraph** (`loop_subgraph.go`): Python-equivalent
`AddLoopNode` macro expansion + condition translation.
- **Multibranch** (`multibranch.go`): `Switch` / `Categorize` routing
via `compose.NewGraphMultiBranch` — same branch selection semantics as
Python.
- **Parallel subgraph** (`parallel_subgraph.go`): matches Python's
parallel fan-out contract.
- **Interrupt/Resume** (`interrupt_resume.go`): `UserFillUpNodeBody` /
`IsInterruptError` / `ExtractInterruptContexts` — replaces the
deprecated Python sentinel chain with eino's native interrupt API,
preserving the same external behavior.
- **Checkpoint** (`checkpoint_store.go`): `RedisCheckPointStore`
Get/Set/Delete, with business metadata (status / canvas_id /
parent_run_id) on a parallel Redis Hash.
- **RunTracker** (`run_tracker.go`): Start / MarkSucceeded / MarkFailed
/ MarkCancelled / AttachCheckpoint — same lifecycle as the Python run
record.
- **Cancel** (`cancel.go`): Redis pub/sub watch.
- **Stream** (`stream.go`): SSE channel with `messages` / `waiting` /
`errors` / `done` events, same shape as Python's `agent.canvas.RunEvent`
payload.
### DSL bridge (`internal/agent/dsl/`)
- `normalize.go`: v1↔v2 collapsed into a single wire format — Python and
Go consume the same stored JSON.
- `reset.go`: per-run state reset matches Python's `Canvas.reset()`
semantics.
- Testdata mirrors Python's `agent_msg.json` / `all.json` / etc.
### Runtime (`internal/agent/runtime/`)
- `CanvasState` / `NewCanvasState` / `GetVar` / `SetVar` / `ReadVars`:
same `{{cpn_id@param}}` resolution model.
- `ResolveTemplate` (regex fast path + gonja fallback) — Python
Jinja-style semantics.
- `selector.go`, `metrics.go`, `component.go`: shared runtime contracts.
## Out of scope (intentionally)
- **`Retrieval` component logic** — wrapped only; full parity lands in a
follow-up PR.
- **Frontend** — only minor dsl-bridge / canvas UX fixes ride along.
- **CLI / admin / model registry** — orthogonal to agent behavior.
## How alignment is verified
`internal/service/agent_run_e2e_test.go` exercises the **full production
chain** against real Python-shaped DSL fixtures:
```
loadCanvasForUser → versionDAO.GetLatest → decodeCanvasFromDSL →
canvas.Compile → cc.Workflow.Invoke → answer extraction
```
using in-memory SQLite + miniredis (no Docker). Covers:
- `TestRunAgent_RealCanvas_BeginMessage` — happy path, `{{sys.query}}`
resolution
- `TestRunAgent_RealCanvas_WaitForUserResume` — two-run resume cycle
(Python-equivalent)
- `TestRunAgent_RealCanvas_CompileFails` — unknown component name →
sanitized error (Python-equivalent)
- `TestRunAgent_RealCanvas_InvokeFails` — unresolvable template ref
(Python-equivalent)
- `TestRunAgent_RunTracker_AttachCheckpoint_CallSequence` —
Start→AttachCheckpoint→MarkSucceeded lifecycle
`internal/handler/agent_test.go` — SSE streaming parity (`Content-Type:
text/event-stream`, `data: {…}\n\n`, trailing `data: [DONE]\n\n`,
OpenAI-compatible non-stream `choices`).
`internal/agent/canvas/fixture_compile_test.go` + per-component tests
pin the Python-equivalent outputs.
```
go test -count=1 -v -run 'TestRunAgent_RealCanvas|TestRunAgent_RunTracker' ./internal/service/
```
## Design reference
`docs/develop/agent-go-port-design.md` (1329 lines, last cross-checked
2026-06-17) — module layout, per-component / per-tool inventory,
corner-case catalogue, and the actionable backlog (Section 14, including
the retrieval alignment follow-up).
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 11:58:29 +08:00
|
|
|
if (!response.ok) {
|
|
|
|
|
let errorMessage = response.statusText || 'Request failed';
|
|
|
|
|
try {
|
|
|
|
|
const errorBody = (await response
|
|
|
|
|
.clone()
|
|
|
|
|
.json()) as Partial<ResponseType>;
|
|
|
|
|
if (typeof errorBody?.message === 'string' && errorBody.message) {
|
|
|
|
|
errorMessage = errorBody.message;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Non-JSON error body; fall back to the HTTP status text.
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
response,
|
|
|
|
|
data: {
|
|
|
|
|
code: response.status,
|
|
|
|
|
data: null,
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
status: response.status,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-06-06 16:30:18 +08:00
|
|
|
|
|
|
|
|
const reader = response?.body
|
|
|
|
|
?.pipeThrough(new TextDecoderStream())
|
|
|
|
|
.pipeThrough(new EventSourceParserStream())
|
|
|
|
|
.getReader();
|
2026-06-24 11:51:24 +08:00
|
|
|
let lastEventData: ResponseType | undefined;
|
2025-06-06 16:30:18 +08:00
|
|
|
|
2026-06-24 11:51:24 +08:00
|
|
|
try {
|
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
|
while (true) {
|
2025-10-23 16:17:28 +08:00
|
|
|
const x = await reader?.read();
|
2026-06-24 11:51:24 +08:00
|
|
|
if (!x) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
const { done, value } = x;
|
|
|
|
|
if (done) {
|
2026-06-26 22:55:49 +08:00
|
|
|
console.log('agent chat sse reader done');
|
2026-06-24 11:51:24 +08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const raw = (value?.data ?? '').trim();
|
|
|
|
|
// SSE end-of-stream sentinel — no payload, skip without
|
|
|
|
|
// surfacing a JSON.parse error to the console.
|
|
|
|
|
if (!raw) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Some upstreams double-wrap the body in a `data:` prefix;
|
|
|
|
|
// strip one layer so JSON.parse sees a real object.
|
|
|
|
|
const payload = raw.startsWith('data:')
|
|
|
|
|
? raw.slice(5).trimStart()
|
|
|
|
|
: raw;
|
|
|
|
|
// Check the sentinel after prefix stripping so a
|
|
|
|
|
// `data: [DONE]` payload is caught and the stream
|
|
|
|
|
// loop is terminated.
|
|
|
|
|
if (payload === '[DONE]') {
|
2026-06-26 22:55:49 +08:00
|
|
|
console.log('agent chat sse done sentinel');
|
2025-10-23 16:17:28 +08:00
|
|
|
break;
|
2025-08-01 09:38:48 +08:00
|
|
|
}
|
2026-06-26 22:55:49 +08:00
|
|
|
const val = normalizeAgentEvent(JSON.parse(payload));
|
|
|
|
|
console.log('agent chat sse event', val);
|
2026-06-24 11:51:24 +08:00
|
|
|
|
|
|
|
|
if (typeof val?.code === 'number' && val.code !== 0) {
|
|
|
|
|
message.error(val.message);
|
2025-10-23 16:17:28 +08:00
|
|
|
}
|
2026-06-24 11:51:24 +08:00
|
|
|
lastEventData = val as ResponseType;
|
|
|
|
|
|
|
|
|
|
setAnswerList((list) => {
|
|
|
|
|
const nextList = [...list];
|
|
|
|
|
nextList.push(val);
|
|
|
|
|
return nextList;
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn(e);
|
2025-06-06 16:30:18 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-24 11:51:24 +08:00
|
|
|
} catch (e) {
|
|
|
|
|
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
|
|
|
console.log('Request was aborted by user or logic.');
|
|
|
|
|
} else {
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
2025-06-06 16:30:18 +08:00
|
|
|
}
|
2026-06-24 11:51:24 +08:00
|
|
|
|
|
|
|
|
const responseData = await responseDataPromise;
|
feat(agent): align Go agent behavior with Python (except retrieval component) (#16225)
## Summary
Aligns the **Go agent runtime/canvas/components/tools** behavior with
the **Python `agent/` implementation** so the same stored canvas DSL
produces the same execution result on either side. Every component,
tool, and runtime primitive in `internal/agent/` is now driven by the
same semantics as its Python counterpart — variable resolution, template
substitution, control flow, error reporting, retry/cancel, and stream
event shapes.
The **retrieval component is the one explicit exception** in this PR. It
is being reworked in a separate change and is excluded from this
alignment pass; the wrapper slot (`universe_a_wrappers.go →
newRetrievalComponent`) is preserved.
## Scope of alignment
### Components (all aligned with `agent/component/`)
`Begin` · `Message` · `LLM` (incl. ChatTemplateKwargs,
MessageHistoryWindowSize, VisualFiles, Cite, OutputStructure,
JSONOutput, TopP, MaxRetries, DelayAfterError, credentials) · `Agent`
(react + tool artifact capture + `Reset()` interface-assert) · `Switch`
(12/12 operators, Python-equivalent semantics) · `Categorize` · `Invoke`
· `Iteration` · `Loop` (macro-expansion through `workflowx.AddLoopNode`)
· `UserFillUp` (Python-equivalent interrupt/resume via eino
`compose.Interrupt`/`ResumeWithData`) · `FillUp` · `DataOperations` ·
`ListOperations` · `StringTransform` · `VariableAggregator` ·
`VariableAssigner` · `Browser` (full stagehand runtime parity) ·
`DocsGenerator` · `ExcelProcessor`.
### Tools (all aligned with `agent/tools/`)
`Retrieval` (wrapper slot only — logic out of scope) · `MCPToolAdapter`
(streamable-HTTP) · `CodeExec` (sandbox bridge with
`code_exec_contract.go` matching Python contract) · `AkShare` · `ArXiv`
· `Crawler` · `DeepL` · `DuckDuckGo` · `Email` · `ExeSQL` · `GitHub` ·
`Google` · `GoogleScholar` · `Jin10` · `PubMed` · `QWeather` · `SearXNG`
· `Tavily` · `Tushare` · `Wencai` · `Wikipedia` · `YahooFinance` —
uniform `eino tool.InvokableTool` interface, SSRF protection, shared
HTTP client.
### Canvas execution engine (`internal/agent/canvas/`)
Aligned with Python's `agent/canvas.py`:
- **Scheduler** (`scheduler.go`): state pre/post handlers, node lambdas,
per-component timeout resolver (4-level: per-class env → per-class table
→ uniform env → 600s fallback), `legacyNoOpNames`.
- **Loop subgraph** (`loop_subgraph.go`): Python-equivalent
`AddLoopNode` macro expansion + condition translation.
- **Multibranch** (`multibranch.go`): `Switch` / `Categorize` routing
via `compose.NewGraphMultiBranch` — same branch selection semantics as
Python.
- **Parallel subgraph** (`parallel_subgraph.go`): matches Python's
parallel fan-out contract.
- **Interrupt/Resume** (`interrupt_resume.go`): `UserFillUpNodeBody` /
`IsInterruptError` / `ExtractInterruptContexts` — replaces the
deprecated Python sentinel chain with eino's native interrupt API,
preserving the same external behavior.
- **Checkpoint** (`checkpoint_store.go`): `RedisCheckPointStore`
Get/Set/Delete, with business metadata (status / canvas_id /
parent_run_id) on a parallel Redis Hash.
- **RunTracker** (`run_tracker.go`): Start / MarkSucceeded / MarkFailed
/ MarkCancelled / AttachCheckpoint — same lifecycle as the Python run
record.
- **Cancel** (`cancel.go`): Redis pub/sub watch.
- **Stream** (`stream.go`): SSE channel with `messages` / `waiting` /
`errors` / `done` events, same shape as Python's `agent.canvas.RunEvent`
payload.
### DSL bridge (`internal/agent/dsl/`)
- `normalize.go`: v1↔v2 collapsed into a single wire format — Python and
Go consume the same stored JSON.
- `reset.go`: per-run state reset matches Python's `Canvas.reset()`
semantics.
- Testdata mirrors Python's `agent_msg.json` / `all.json` / etc.
### Runtime (`internal/agent/runtime/`)
- `CanvasState` / `NewCanvasState` / `GetVar` / `SetVar` / `ReadVars`:
same `{{cpn_id@param}}` resolution model.
- `ResolveTemplate` (regex fast path + gonja fallback) — Python
Jinja-style semantics.
- `selector.go`, `metrics.go`, `component.go`: shared runtime contracts.
## Out of scope (intentionally)
- **`Retrieval` component logic** — wrapped only; full parity lands in a
follow-up PR.
- **Frontend** — only minor dsl-bridge / canvas UX fixes ride along.
- **CLI / admin / model registry** — orthogonal to agent behavior.
## How alignment is verified
`internal/service/agent_run_e2e_test.go` exercises the **full production
chain** against real Python-shaped DSL fixtures:
```
loadCanvasForUser → versionDAO.GetLatest → decodeCanvasFromDSL →
canvas.Compile → cc.Workflow.Invoke → answer extraction
```
using in-memory SQLite + miniredis (no Docker). Covers:
- `TestRunAgent_RealCanvas_BeginMessage` — happy path, `{{sys.query}}`
resolution
- `TestRunAgent_RealCanvas_WaitForUserResume` — two-run resume cycle
(Python-equivalent)
- `TestRunAgent_RealCanvas_CompileFails` — unknown component name →
sanitized error (Python-equivalent)
- `TestRunAgent_RealCanvas_InvokeFails` — unresolvable template ref
(Python-equivalent)
- `TestRunAgent_RunTracker_AttachCheckpoint_CallSequence` —
Start→AttachCheckpoint→MarkSucceeded lifecycle
`internal/handler/agent_test.go` — SSE streaming parity (`Content-Type:
text/event-stream`, `data: {…}\n\n`, trailing `data: [DONE]\n\n`,
OpenAI-compatible non-stream `choices`).
`internal/agent/canvas/fixture_compile_test.go` + per-component tests
pin the Python-equivalent outputs.
```
go test -count=1 -v -run 'TestRunAgent_RealCanvas|TestRunAgent_RunTracker' ./internal/service/
```
## Design reference
`docs/develop/agent-go-port-design.md` (1329 lines, last cross-checked
2026-06-17) — module layout, per-component / per-tool inventory,
corner-case catalogue, and the actionable backlog (Section 14, including
the retrieval alignment follow-up).
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 11:58:29 +08:00
|
|
|
return {
|
|
|
|
|
response,
|
2026-06-24 11:51:24 +08:00
|
|
|
data:
|
|
|
|
|
responseData ??
|
|
|
|
|
lastEventData ??
|
|
|
|
|
({
|
|
|
|
|
code: 0,
|
|
|
|
|
data: true,
|
|
|
|
|
message: 'success',
|
|
|
|
|
status: response.status,
|
|
|
|
|
} as ResponseType),
|
feat(agent): align Go agent behavior with Python (except retrieval component) (#16225)
## Summary
Aligns the **Go agent runtime/canvas/components/tools** behavior with
the **Python `agent/` implementation** so the same stored canvas DSL
produces the same execution result on either side. Every component,
tool, and runtime primitive in `internal/agent/` is now driven by the
same semantics as its Python counterpart — variable resolution, template
substitution, control flow, error reporting, retry/cancel, and stream
event shapes.
The **retrieval component is the one explicit exception** in this PR. It
is being reworked in a separate change and is excluded from this
alignment pass; the wrapper slot (`universe_a_wrappers.go →
newRetrievalComponent`) is preserved.
## Scope of alignment
### Components (all aligned with `agent/component/`)
`Begin` · `Message` · `LLM` (incl. ChatTemplateKwargs,
MessageHistoryWindowSize, VisualFiles, Cite, OutputStructure,
JSONOutput, TopP, MaxRetries, DelayAfterError, credentials) · `Agent`
(react + tool artifact capture + `Reset()` interface-assert) · `Switch`
(12/12 operators, Python-equivalent semantics) · `Categorize` · `Invoke`
· `Iteration` · `Loop` (macro-expansion through `workflowx.AddLoopNode`)
· `UserFillUp` (Python-equivalent interrupt/resume via eino
`compose.Interrupt`/`ResumeWithData`) · `FillUp` · `DataOperations` ·
`ListOperations` · `StringTransform` · `VariableAggregator` ·
`VariableAssigner` · `Browser` (full stagehand runtime parity) ·
`DocsGenerator` · `ExcelProcessor`.
### Tools (all aligned with `agent/tools/`)
`Retrieval` (wrapper slot only — logic out of scope) · `MCPToolAdapter`
(streamable-HTTP) · `CodeExec` (sandbox bridge with
`code_exec_contract.go` matching Python contract) · `AkShare` · `ArXiv`
· `Crawler` · `DeepL` · `DuckDuckGo` · `Email` · `ExeSQL` · `GitHub` ·
`Google` · `GoogleScholar` · `Jin10` · `PubMed` · `QWeather` · `SearXNG`
· `Tavily` · `Tushare` · `Wencai` · `Wikipedia` · `YahooFinance` —
uniform `eino tool.InvokableTool` interface, SSRF protection, shared
HTTP client.
### Canvas execution engine (`internal/agent/canvas/`)
Aligned with Python's `agent/canvas.py`:
- **Scheduler** (`scheduler.go`): state pre/post handlers, node lambdas,
per-component timeout resolver (4-level: per-class env → per-class table
→ uniform env → 600s fallback), `legacyNoOpNames`.
- **Loop subgraph** (`loop_subgraph.go`): Python-equivalent
`AddLoopNode` macro expansion + condition translation.
- **Multibranch** (`multibranch.go`): `Switch` / `Categorize` routing
via `compose.NewGraphMultiBranch` — same branch selection semantics as
Python.
- **Parallel subgraph** (`parallel_subgraph.go`): matches Python's
parallel fan-out contract.
- **Interrupt/Resume** (`interrupt_resume.go`): `UserFillUpNodeBody` /
`IsInterruptError` / `ExtractInterruptContexts` — replaces the
deprecated Python sentinel chain with eino's native interrupt API,
preserving the same external behavior.
- **Checkpoint** (`checkpoint_store.go`): `RedisCheckPointStore`
Get/Set/Delete, with business metadata (status / canvas_id /
parent_run_id) on a parallel Redis Hash.
- **RunTracker** (`run_tracker.go`): Start / MarkSucceeded / MarkFailed
/ MarkCancelled / AttachCheckpoint — same lifecycle as the Python run
record.
- **Cancel** (`cancel.go`): Redis pub/sub watch.
- **Stream** (`stream.go`): SSE channel with `messages` / `waiting` /
`errors` / `done` events, same shape as Python's `agent.canvas.RunEvent`
payload.
### DSL bridge (`internal/agent/dsl/`)
- `normalize.go`: v1↔v2 collapsed into a single wire format — Python and
Go consume the same stored JSON.
- `reset.go`: per-run state reset matches Python's `Canvas.reset()`
semantics.
- Testdata mirrors Python's `agent_msg.json` / `all.json` / etc.
### Runtime (`internal/agent/runtime/`)
- `CanvasState` / `NewCanvasState` / `GetVar` / `SetVar` / `ReadVars`:
same `{{cpn_id@param}}` resolution model.
- `ResolveTemplate` (regex fast path + gonja fallback) — Python
Jinja-style semantics.
- `selector.go`, `metrics.go`, `component.go`: shared runtime contracts.
## Out of scope (intentionally)
- **`Retrieval` component logic** — wrapped only; full parity lands in a
follow-up PR.
- **Frontend** — only minor dsl-bridge / canvas UX fixes ride along.
- **CLI / admin / model registry** — orthogonal to agent behavior.
## How alignment is verified
`internal/service/agent_run_e2e_test.go` exercises the **full production
chain** against real Python-shaped DSL fixtures:
```
loadCanvasForUser → versionDAO.GetLatest → decodeCanvasFromDSL →
canvas.Compile → cc.Workflow.Invoke → answer extraction
```
using in-memory SQLite + miniredis (no Docker). Covers:
- `TestRunAgent_RealCanvas_BeginMessage` — happy path, `{{sys.query}}`
resolution
- `TestRunAgent_RealCanvas_WaitForUserResume` — two-run resume cycle
(Python-equivalent)
- `TestRunAgent_RealCanvas_CompileFails` — unknown component name →
sanitized error (Python-equivalent)
- `TestRunAgent_RealCanvas_InvokeFails` — unresolvable template ref
(Python-equivalent)
- `TestRunAgent_RunTracker_AttachCheckpoint_CallSequence` —
Start→AttachCheckpoint→MarkSucceeded lifecycle
`internal/handler/agent_test.go` — SSE streaming parity (`Content-Type:
text/event-stream`, `data: {…}\n\n`, trailing `data: [DONE]\n\n`,
OpenAI-compatible non-stream `choices`).
`internal/agent/canvas/fixture_compile_test.go` + per-component tests
pin the Python-equivalent outputs.
```
go test -count=1 -v -run 'TestRunAgent_RealCanvas|TestRunAgent_RunTracker' ./internal/service/
```
## Design reference
`docs/develop/agent-go-port-design.md` (1329 lines, last cross-checked
2026-06-17) — module layout, per-component / per-tool inventory,
corner-case catalogue, and the actionable backlog (Section 14, including
the retrieval alignment follow-up).
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-22 11:58:29 +08:00
|
|
|
};
|
2025-06-06 16:30:18 +08:00
|
|
|
} catch (e) {
|
2026-06-24 11:51:24 +08:00
|
|
|
console.warn(e);
|
|
|
|
|
return undefined;
|
|
|
|
|
} finally {
|
2025-06-06 16:30:18 +08:00
|
|
|
setDone(true);
|
|
|
|
|
resetAnswerList();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[initializeSseRef, url, resetAnswerList],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const stopOutputMessage = useCallback(() => {
|
|
|
|
|
sseRef.current?.abort();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
send,
|
|
|
|
|
answerList,
|
|
|
|
|
done,
|
|
|
|
|
setDone,
|
|
|
|
|
resetAnswerList,
|
|
|
|
|
stopOutputMessage,
|
|
|
|
|
};
|
|
|
|
|
};
|