feat(agent): ship the Go agent canvas port — eino interrupt/resume + Redis check-pointing (#16035)

Replaces the Python agent canvas runtime with a Go implementation that
runs inside `cmd/server_main`.

The canvas compiles into an eino Workflow that pauses on wait-for-user
via native Interrupt/Resume (no sentinel flag) and resumes from a
Redis-backed CheckPointStore.

All 21 Python agent components and ~35 tools are ported with functional
parity.

Sandbox providers now read their JSON config from the admin-panel
system_settings table with env fallback.

234 files / +35,413 / -6,111. All Go files are gofmt-clean (CI gate
added); drops the v2 DSL E2E step and the gap-analysis plan (both
redundant after the port ships).

## Type of change

- [x] Refactoring
- [x] New feature
- [x] Bug fix

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Zhichang Yu
2026-06-17 13:24:03 +08:00
committed by GitHub
parent 2290bb0023
commit e45659868a
231 changed files with 33807 additions and 6114 deletions

View File

@@ -11,23 +11,30 @@ import { get, isEmpty } from 'lodash';
import { ReactNode } from 'react';
export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
return nodeIds.reduce<string[]>((pre, nodeId) => {
const currentEdges = edges.filter((x) => x.target === nodeId);
const upstreamNodeIds: string[] = currentEdges.map((x) => x.source);
const ids = upstreamNodeIds.concat(
filterAllUpstreamNodeIds(edges, upstreamNodeIds),
);
ids.forEach((x) => {
if (pre.every((y) => y !== x)) {
pre.push(x);
// Iterative BFS with a visited set so cycles in the upstream graph
// (e.g. answer:0 ↔ exesql:0 in the v1 exesql.json fixture) cannot
// recurse forever. The previous recursive implementation had no cycle
// detection and would blow the stack on any cyclic dsl.
const visited = new Set<string>(nodeIds);
const result: string[] = [];
let frontier = [...nodeIds];
while (frontier.length) {
const next: string[] = [];
for (const nodeId of frontier) {
const upstreamIds = edges
.filter((x) => x.target === nodeId)
.map((x) => x.source);
for (const id of upstreamIds) {
if (!visited.has(id)) {
visited.add(id);
result.push(id);
next.push(id);
}
}
});
return pre;
}, []);
}
frontier = next;
}
return result;
}
export function filterChildNodeIds(nodes: BaseNode[], nodeId?: string) {

View File

@@ -168,10 +168,52 @@ export const downloadJsonFile = async (
data: Record<string, any>,
fileName: string,
) => {
const blob = new Blob([JSON.stringify(data)], { type: FileMimeType.Json });
// Pretty-print with 2-space indent + sort keys at every depth so
// the downloaded file is human-readable AND byte-stable across
// re-exports: a user exporting the same canvas twice gets the
// exact same bytes (modulo round-trip edits), which makes the
// file easy to diff in version control and easy to hand-edit.
// Mirrors the `sort_keys=True` we apply to the testdata fixtures
// fixtures, so an exported dsl imported in v2 mode and re-
// exported stays identical at the byte level.
//
// JSON.stringify already leaves non-ASCII (e.g. the Chinese
// prompts we store under `Browser.prompts`) un-escaped by
// default, so no `ensure_ascii` toggle is needed.
const blob = new Blob([JSON.stringify(sortKeysDeep(data), null, 2)], {
type: FileMimeType.Json,
});
downloadFileFromBlob(blob, fileName);
};
// sortKeysDeep returns a structural copy of `value` with every
// plain-object key sorted alphabetically. Array element order is
// preserved (semantic — nodes/edges are not interchangeable), but
// each element is recursively sorted if it is itself an object.
// Primitives, `null`, and non-plain objects are passed through
// unchanged. Used to make the exported dsl byte-stable: JSON
// property iteration order is implementation-defined in JS, and
// React Flow nodes carry fields in a stable order today but we
// don't want to depend on that.
const sortKeysDeep = (value: any): any => {
if (Array.isArray(value)) {
return value.map(sortKeysDeep);
}
if (value !== null && typeof value === 'object') {
// Only sort plain {…} objects — skip Date, RegExp, Map, etc.
if (Object.getPrototypeOf(value) !== Object.prototype) {
return value;
}
return Object.keys(value)
.sort()
.reduce<Record<string, any>>((acc, key) => {
acc[key] = sortKeysDeep(value[key]);
return acc;
}, {});
}
return value;
};
export function transformBase64ToFileWithPreview(
dataUrl: string,
filename: string = 'file',