create-windy 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/cli.js +52 -34
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/src/southwind_agent_server/attempt_context.py +9 -1
  5. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +5 -2
  6. package/template/apps/agent-server/src/southwind_agent_server/provider_output_guidance.py +34 -0
  7. package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +16 -5
  8. package/template/apps/agent-server/tests/test_structured_logging.py +37 -0
  9. package/template/apps/agent-server/tests/test_tool_structured_output.py +347 -0
  10. package/template/apps/server/src/agent/admin-catalog-bootstrap.ts +25 -0
  11. package/template/apps/server/src/agent/agent-session-bootstrap.ts +7 -8
  12. package/template/apps/server/src/agent/agent-session-facade.ts +12 -0
  13. package/template/apps/server/src/agent/agent-session-validation.ts +43 -0
  14. package/template/apps/server/src/agent/operation-catalog.test.ts +202 -0
  15. package/template/apps/server/src/agent/operation-catalog.ts +135 -0
  16. package/template/apps/server/src/agent/run-browser-visibility.test.ts +59 -0
  17. package/template/apps/server/src/agent/run-browser-visibility.ts +11 -0
  18. package/template/apps/server/src/agent/run-operation-policy.ts +1 -0
  19. package/template/apps/server/src/agent/run-route-response.ts +35 -0
  20. package/template/apps/server/src/agent/run-routes.ts +46 -43
  21. package/template/apps/server/src/agent/runtime-bootstrap.ts +21 -20
  22. package/template/apps/server/src/agent/runtime-disabled-composition.ts +28 -0
  23. package/template/apps/server/src/agent/session-projection-facade.ts +8 -9
  24. package/template/apps/server/src/agent/session-projection-governance.ts +10 -0
  25. package/template/apps/server/src/agent/session-projection-view.ts +19 -16
  26. package/template/apps/server/src/agent/session-recording-reader.contract.test.ts +131 -0
  27. package/template/apps/server/src/agent/session-recording-reader.ts +62 -0
  28. package/template/apps/server/src/agent/tool-catalog-audit.ts +22 -0
  29. package/template/apps/server/src/agent/tool-catalog-availability.ts +21 -0
  30. package/template/apps/server/src/agent/tool-catalog-routes.test.ts +103 -0
  31. package/template/apps/server/src/agent/tool-catalog-routes.ts +62 -0
  32. package/template/apps/server/src/agent/tool-catalog.test.ts +122 -0
  33. package/template/apps/server/src/agent/tool-catalog.ts +44 -0
  34. package/template/apps/server/src/agent/tool-registry.ts +45 -14
  35. package/template/apps/server/src/agent/workbench-bootstrap.ts +39 -0
  36. package/template/apps/server/src/agent/workbench-event-projection.ts +115 -0
  37. package/template/apps/server/src/agent/workbench-facade.contract.test.ts +189 -0
  38. package/template/apps/server/src/agent/workbench-facade.ts +339 -0
  39. package/template/apps/server/src/agent/workbench-recording.contract.test.ts +328 -0
  40. package/template/apps/server/src/agent/workbench-routes.ts +97 -0
  41. package/template/apps/server/src/agent/workbench-run-integration.test.ts +231 -0
  42. package/template/apps/server/src/agent/workbench-run-reader.ts +187 -0
  43. package/template/apps/server/src/agent/workbench-safe-result.ts +61 -0
  44. package/template/apps/server/src/agent/workbench-sse.ts +50 -0
  45. package/template/apps/server/src/agent-workflow/query-service.ts +36 -0
  46. package/template/apps/server/src/agent-workflow/recording-projection.contract.test.ts +196 -0
  47. package/template/apps/server/src/agent-workflow/recording-projection.ts +86 -0
  48. package/template/apps/web/src/pages/system/AgentDebugPage.vue +14 -0
  49. package/template/apps/web/src/pages/system/AgentSessionPage.vue +29 -8
  50. package/template/apps/web/src/pages/system/AgentToolsPage.vue +76 -0
  51. package/template/apps/web/src/pages/system/components/agent-workbench/AgentWorkbench.vue +236 -0
  52. package/template/apps/web/src/pages/system/components/agent-workbench/DraftLeaveDialog.vue +34 -0
  53. package/template/apps/web/src/pages/system/components/agent-workbench/ReplayEventDetail.vue +175 -0
  54. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +142 -0
  55. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.webtest.ts +242 -0
  56. package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +97 -0
  57. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +100 -0
  58. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +55 -0
  59. package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +81 -0
  60. package/template/apps/web/src/pages/system/components/agent-workbench/WorkbenchSessionList.vue +57 -0
  61. package/template/apps/web/src/pages/system/components/agent-workbench/catalog-presenter.ts +20 -0
  62. package/template/apps/web/src/pages/system/components/agent-workbench/replay-presenter.ts +94 -0
  63. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.ts +298 -0
  64. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts +164 -0
  65. package/template/apps/web/src/pages/system/composables/useSessionReplay.ts +48 -0
  66. package/template/apps/web/src/router/index.ts +4 -0
  67. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  68. package/template/apps/web/src/services/agent-admin-api.ts +155 -0
  69. package/template/apps/web/src/services/agent-admin-api.webtest.ts +120 -0
  70. package/template/apps/web/src/services/agent-workbench-event.ts +60 -0
  71. package/template/docs/architecture/agent-harness.md +4 -0
  72. package/template/docs/architecture/log-governance-implementation.md +20 -3
  73. package/template/docs/architecture/log-governance-plan.md +2 -2
  74. package/template/docs/platform/agent-runtime.md +18 -0
  75. package/template/packages/modules/src/ai-operation-composition.ts +9 -0
  76. package/template/packages/modules/src/ai-operation-visibility.test.ts +33 -0
  77. package/template/packages/modules/src/manifest.ts +2 -0
  78. package/template/packages/modules/src/system-admin-routes.ts +16 -0
  79. package/template/packages/modules/src/system-admin-ui-catalog.ts +7 -1
  80. package/template/packages/modules/src/system-agent-admin-bindings.ts +42 -0
  81. package/template/packages/modules/src/system-agent-admin-menus.ts +26 -0
  82. package/template/packages/modules/src/system-agent-admin-registration.test.ts +52 -0
  83. package/template/packages/modules/src/system-agent-audit-actions.ts +5 -0
  84. package/template/packages/modules/src/system-ai-api-bindings.ts +2 -0
  85. package/template/packages/modules/src/system-audit-actions.ts +25 -0
  86. package/template/packages/modules/src/system-features.ts +19 -0
  87. package/template/packages/modules/src/system-module-catalog.ts +14 -12
  88. package/template/packages/modules/src/system-module-ownership.ts +12 -0
  89. package/template/packages/modules/src/system-modules.ts +2 -0
  90. package/template/packages/modules/src/system-permissions.ts +5 -0
  91. package/template/packages/server-sdk/index.ts +2 -0
  92. package/template/packages/server-sdk/src/agent-session-provider.ts +19 -0
  93. package/template/packages/shared/index.ts +2 -0
  94. package/template/packages/shared/src/agent-admin-catalog.ts +73 -0
  95. package/template/packages/shared/src/agent-workbench.ts +100 -0
@@ -0,0 +1,164 @@
1
+ import { defineComponent, shallowRef } from "vue";
2
+ import { flushPromises, mount } from "@vue/test-utils";
3
+ import { beforeEach, describe, expect, test, vi } from "vitest";
4
+ import type { AgentWorkbenchRecordingV1 } from "@southwind-ai/shared";
5
+ const mocks = vi.hoisted(() => ({
6
+ list: vi.fn(),
7
+ operations: vi.fn(),
8
+ detail: vi.fn(),
9
+ session: vi.fn(),
10
+ run: vi.fn(),
11
+ stream: vi.fn(),
12
+ result: vi.fn(),
13
+ cancel: vi.fn(),
14
+ }));
15
+ vi.mock("@/services/agent-admin-api", () => ({
16
+ listWorkbenchRecordings: mocks.list,
17
+ listAdminOperations: mocks.operations,
18
+ getWorkbenchRecording: mocks.detail,
19
+ createTestSession: mocks.session,
20
+ createTestRun: mocks.run,
21
+ streamTestRun: mocks.stream,
22
+ getTestRunResult: mocks.result,
23
+ cancelTestRun: mocks.cancel,
24
+ }));
25
+ const fingerprint = shallowRef("actor-one");
26
+ vi.mock("@/composables/useAccessSnapshot", () => ({
27
+ useAccessSnapshot: () => ({
28
+ fingerprint,
29
+ snapshot: shallowRef({
30
+ actor: { permissionKeys: ["system.agent-debug.manage"] },
31
+ }),
32
+ }),
33
+ }));
34
+ vi.mock("@/composables/useAuthSession", () => ({
35
+ useAuthSession: () => ({ actor: shallowRef({ id: "actor-one" }) }),
36
+ }));
37
+ import { useAgentWorkbench } from "./useAgentWorkbench";
38
+ const history: AgentWorkbenchRecordingV1 = {
39
+ source: "harness",
40
+ sessionId: "session_test",
41
+ title: "测试",
42
+ updatedAt: "today",
43
+ mode: "read-only",
44
+ sideEffectPolicy: "no-reexecution",
45
+ correlationStatus: "complete",
46
+ timeline: [],
47
+ messages: [],
48
+ runs: [],
49
+ workflows: [],
50
+ missing: [],
51
+ };
52
+ function setup() {
53
+ let state!: ReturnType<typeof useAgentWorkbench>;
54
+ const wrapper = mount(
55
+ defineComponent({
56
+ setup() {
57
+ state = useAgentWorkbench();
58
+ return () => null;
59
+ },
60
+ }),
61
+ );
62
+ return { state, wrapper };
63
+ }
64
+ beforeEach(() => {
65
+ fingerprint.value = "actor-one";
66
+ vi.clearAllMocks();
67
+ mocks.list.mockResolvedValue({ items: [] });
68
+ mocks.operations.mockResolvedValue([]);
69
+ mocks.detail.mockResolvedValue(history);
70
+ mocks.session.mockResolvedValue({ sessionId: "session_test" });
71
+ mocks.run.mockResolvedValue({ runId: "run_test", status: "accepted" });
72
+ mocks.result.mockResolvedValue({
73
+ status: "completed",
74
+ output: { total: 12 },
75
+ });
76
+ mocks.cancel.mockResolvedValue({});
77
+ mocks.stream.mockImplementation(
78
+ async (_run, _operation, _session, onEvent) => {
79
+ onEvent({ sequence: 1, type: "run.completed", summary: "已完成" });
80
+ },
81
+ );
82
+ });
83
+ describe("真实测试工作台编排", () => {
84
+ test("选中的历史 canonical Session 只读,另起运行使用新 Session", async () => {
85
+ const { state, wrapper } = setup();
86
+ await flushPromises();
87
+ await state.select({
88
+ source: "harness",
89
+ sessionId: "session_historical",
90
+ title: "历史",
91
+ updatedAt: "today",
92
+ });
93
+ expect(mocks.run).not.toHaveBeenCalled();
94
+ await state.start("finance.query", {});
95
+ await flushPromises();
96
+ expect(mocks.session).toHaveBeenCalledTimes(1);
97
+ expect(mocks.run.mock.calls[0]?.[2]).toBe("session_test");
98
+ wrapper.unmount();
99
+ });
100
+
101
+ test("使用常规治理API,成功结果后同一Session可以再运行,不重复建Session", async () => {
102
+ const { state, wrapper } = setup();
103
+ await flushPromises();
104
+ expect(await state.start("finance.query", { request: "sum" })).toBe(true);
105
+ await flushPromises();
106
+ expect(state.output.value).toEqual({ total: 12 });
107
+ expect(await state.start("finance.query", { request: "next" })).toBe(true);
108
+ await flushPromises();
109
+ expect(mocks.session).toHaveBeenCalledTimes(1);
110
+ expect(mocks.run).toHaveBeenCalledTimes(2);
111
+ expect(mocks.run.mock.calls[1]?.slice(0, 3)).toEqual([
112
+ "finance.query",
113
+ { request: "next" },
114
+ "session_test",
115
+ ]);
116
+ wrapper.unmount();
117
+ });
118
+ test("只读业务选择不会启动模型;另起测试使用独立Session", async () => {
119
+ const { state, wrapper } = setup();
120
+ await flushPromises();
121
+ await state.select({
122
+ source: "business",
123
+ sessionId: "copilot_one",
124
+ title: "业务",
125
+ updatedAt: "today",
126
+ });
127
+ expect(mocks.run).not.toHaveBeenCalled();
128
+ expect(mocks.stream).not.toHaveBeenCalled();
129
+ expect(mocks.result).not.toHaveBeenCalled();
130
+ await state.start("finance.query", {});
131
+ await flushPromises();
132
+ expect(mocks.run.mock.calls[0]?.[2]).toBe("session_test");
133
+ wrapper.unmount();
134
+ });
135
+ test("手动取消运行与离开detach分开,授权变化会隐藏旧记录", async () => {
136
+ mocks.stream.mockImplementation(() => new Promise(() => {}));
137
+ const { state, wrapper } = setup();
138
+ await flushPromises();
139
+ await state.start("finance.query", {});
140
+ await state.cancel();
141
+ expect(mocks.cancel.mock.calls[0]?.slice(0, 3)).toEqual([
142
+ "run_test",
143
+ "finance.query",
144
+ "session_test",
145
+ ]);
146
+ fingerprint.value = "actor-two";
147
+ await flushPromises();
148
+ expect(state.liveRun.value).toBeUndefined();
149
+ expect(state.recording.value).toBeUndefined();
150
+ expect(state.sessions.value).toEqual([]);
151
+ wrapper.unmount();
152
+ expect(mocks.cancel).toHaveBeenCalledTimes(1);
153
+ });
154
+ test("新建失败保留错误且不自动重试", async () => {
155
+ mocks.run.mockRejectedValue(new Error("Runtime不可用"));
156
+ const { state, wrapper } = setup();
157
+ await flushPromises();
158
+ expect(await state.start("finance.query", {})).toBe(false);
159
+ await flushPromises();
160
+ expect(state.error.value).toContain("Runtime不可用");
161
+ expect(mocks.run).toHaveBeenCalledTimes(1);
162
+ wrapper.unmount();
163
+ });
164
+ });
@@ -0,0 +1,48 @@
1
+ import { computed, onBeforeUnmount, shallowRef, watch, type Ref } from "vue";
2
+ import type { AgentWorkbenchEventV1 } from "@southwind-ai/shared";
3
+ /** Local playback changes only the visible cursor; it never calls a runtime. */
4
+ export function useSessionReplay(
5
+ events: Readonly<Ref<readonly AgentWorkbenchEventV1[]>>,
6
+ ) {
7
+ const cursor = shallowRef(-1);
8
+ const playing = shallowRef(false);
9
+ const speed = shallowRef(1);
10
+ let timer: ReturnType<typeof setTimeout> | undefined;
11
+ const current = computed(() => events.value[cursor.value]);
12
+ const visible = computed(() => events.value.slice(0, cursor.value + 1));
13
+ function pause() {
14
+ playing.value = false;
15
+ if (timer) clearTimeout(timer);
16
+ timer = undefined;
17
+ }
18
+ function seek(index: number) {
19
+ pause();
20
+ cursor.value = Math.max(-1, Math.min(index, events.value.length - 1));
21
+ }
22
+ function advance() {
23
+ if (!playing.value) return;
24
+ if (cursor.value >= events.value.length - 1) {
25
+ pause();
26
+ return;
27
+ }
28
+ cursor.value += 1;
29
+ timer = setTimeout(advance, 1000 / speed.value);
30
+ }
31
+ function play() {
32
+ if (cursor.value >= events.value.length - 1) cursor.value = -1;
33
+ playing.value = true;
34
+ advance();
35
+ }
36
+ watch(events, () => {
37
+ pause();
38
+ cursor.value = events.value.length ? 0 : -1;
39
+ });
40
+ watch(speed, () => {
41
+ if (playing.value) {
42
+ pause();
43
+ play();
44
+ }
45
+ });
46
+ onBeforeUnmount(pause);
47
+ return { cursor, playing, speed, current, visible, seek, pause, play };
48
+ }
@@ -20,6 +20,8 @@ import SystemOperationsPage from "@/pages/system/SystemOperationsPage.vue";
20
20
  // @windy-module system.operations end
21
21
  // @windy-module system.agent begin
22
22
  import AiRuntimePage from "@/pages/system/AiRuntimePage.vue";
23
+ import AgentToolsPage from "@/pages/system/AgentToolsPage.vue";
24
+ import AgentDebugPage from "@/pages/system/AgentDebugPage.vue";
23
25
  import AgentSessionPage from "@/pages/system/AgentSessionPage.vue";
24
26
  import AgentWorkflowPage from "@/pages/system/AgentWorkflowPage.vue";
25
27
  // @windy-module system.agent end
@@ -101,6 +103,8 @@ const platformAdminPageComponents: Partial<
101
103
  operations: SystemOperationsPage,
102
104
  // @windy-module system.operations end
103
105
  // @windy-module system.agent begin
106
+ "agent-tools": AgentToolsPage,
107
+ "agent-debug": AgentDebugPage,
104
108
  "agent-session": AgentSessionPage,
105
109
  "agent-workflow": AgentWorkflowPage,
106
110
  "ai-runtime": AiRuntimePage,
@@ -8,6 +8,8 @@ export type PlatformAdminPageKey =
8
8
  | "notification"
9
9
  | "operations"
10
10
  // @windy-module system.agent begin
11
+ | "agent-tools"
12
+ | "agent-debug"
11
13
  | "agent-session"
12
14
  | "agent-workflow"
13
15
  | "ai-runtime"
@@ -60,6 +62,8 @@ export const platformAdminRouteBindings: readonly PlatformAdminRouteBinding[] =
60
62
  { routeKey: "system.ops", page: "operations" },
61
63
  // @windy-module system.operations end
62
64
  // @windy-module system.agent begin
65
+ { routeKey: "system.agent-tools", page: "agent-tools" },
66
+ { routeKey: "system.agent-debug", page: "agent-debug" },
63
67
  { routeKey: "system.agent-session", page: "agent-session" },
64
68
  { routeKey: "system.agent-workflow", page: "agent-workflow" },
65
69
  { routeKey: "system.ai-runtime", page: "ai-runtime" },
@@ -0,0 +1,155 @@
1
+ import type {
2
+ AgentAdminOperationCatalogItem,
3
+ AgentAdminToolCatalogItem,
4
+ AgentWorkbenchRecordingSummaryV1,
5
+ AgentWorkbenchRecordingV1,
6
+ AgentWorkbenchEventV1,
7
+ JsonValue,
8
+ } from "@southwind-ai/shared";
9
+ import { apiRequest, apiResponse } from "./http";
10
+ import { publicWorkbenchEvent } from "./agent-workbench-event";
11
+
12
+ const policy = { suppressFailureHandler: true };
13
+ export function listAdminTools(signal?: AbortSignal) {
14
+ return apiRequest<readonly AgentAdminToolCatalogItem[]>(
15
+ "/agent/admin/tools",
16
+ { signal },
17
+ policy,
18
+ );
19
+ }
20
+ export function listAdminOperations(signal?: AbortSignal) {
21
+ return apiRequest<readonly AgentAdminOperationCatalogItem[]>(
22
+ "/agent/admin/operations",
23
+ { signal },
24
+ policy,
25
+ );
26
+ }
27
+ export function listWorkbenchRecordings(
28
+ source: "harness" | "business",
29
+ signal?: AbortSignal,
30
+ ) {
31
+ return apiRequest<{ items: readonly AgentWorkbenchRecordingSummaryV1[] }>(
32
+ `/agent/admin/session-recordings?source=${source}`,
33
+ { signal },
34
+ policy,
35
+ );
36
+ }
37
+ export function getWorkbenchRecording(
38
+ sessionId: string,
39
+ source: "harness" | "business",
40
+ signal?: AbortSignal,
41
+ ) {
42
+ return apiRequest<AgentWorkbenchRecordingV1>(
43
+ `/agent/admin/session-recordings/${encodeURIComponent(sessionId)}?source=${source}`,
44
+ { signal },
45
+ policy,
46
+ );
47
+ }
48
+ export function createTestSession(signal?: AbortSignal) {
49
+ return apiRequest<{ sessionId: string }>(
50
+ "/agent/admin/test-sessions",
51
+ { method: "POST", body: "{}", signal },
52
+ policy,
53
+ );
54
+ }
55
+ export function createTestRun(
56
+ operationKey: string,
57
+ input: JsonValue,
58
+ sessionId: string,
59
+ signal?: AbortSignal,
60
+ ) {
61
+ return apiRequest<{ runId: string; status: string }>(
62
+ "/agent/runs",
63
+ {
64
+ method: "POST",
65
+ body: JSON.stringify({ operationKey, input, sessionId }),
66
+ headers: { "idempotency-key": crypto.randomUUID() },
67
+ signal,
68
+ },
69
+ policy,
70
+ );
71
+ }
72
+ function runPath(
73
+ runId: string,
74
+ operationKey: string,
75
+ action: string,
76
+ sessionId: string,
77
+ ) {
78
+ return `/agent/runs/${encodeURIComponent(runId)}/${action}?${new URLSearchParams({ operationKey, sessionId })}`;
79
+ }
80
+ export function getTestRunResult(
81
+ runId: string,
82
+ operationKey: string,
83
+ sessionId: string,
84
+ signal?: AbortSignal,
85
+ ) {
86
+ return apiRequest<{ status: string; output?: JsonValue }>(
87
+ runPath(runId, operationKey, "result", sessionId),
88
+ { signal },
89
+ policy,
90
+ );
91
+ }
92
+ export function cancelTestRun(
93
+ runId: string,
94
+ operationKey: string,
95
+ sessionId: string,
96
+ signal?: AbortSignal,
97
+ ) {
98
+ return apiRequest<unknown>(
99
+ runPath(runId, operationKey, "cancel", sessionId),
100
+ {
101
+ method: "POST",
102
+ body: JSON.stringify({ operationKey, sessionId }),
103
+ signal,
104
+ },
105
+ policy,
106
+ );
107
+ }
108
+ /** The server publishes a controlled summary; never retain arbitrary provider payloads. */
109
+ export async function streamTestRun(
110
+ runId: string,
111
+ operationKey: string,
112
+ sessionId: string,
113
+ onEvent: (event: AgentWorkbenchEventV1) => void,
114
+ signal: AbortSignal,
115
+ ) {
116
+ const response = await apiResponse(
117
+ runPath(runId, operationKey, "events", sessionId),
118
+ { signal, headers: { accept: "text/event-stream" } },
119
+ policy,
120
+ );
121
+ if (!response.body) throw new Error("运行事件流不可用,请手动刷新会话");
122
+ const reader = response.body.getReader();
123
+ const decoder = new TextDecoder();
124
+ let buffer = "";
125
+ try {
126
+ while (!signal.aborted) {
127
+ const chunk = await reader.read();
128
+ if (chunk.done) break;
129
+ buffer += decoder.decode(chunk.value, { stream: true });
130
+ buffer = buffer.replace(/\r\n/g, "\n");
131
+ let boundary = buffer.indexOf("\n\n");
132
+ while (boundary !== -1) {
133
+ if (boundary > 1_048_576)
134
+ throw new Error("事件流超出安全大小,请手动刷新");
135
+ const frame = buffer.slice(0, boundary);
136
+ buffer = buffer.slice(boundary + 2);
137
+ const data = frame
138
+ .split("\n")
139
+ .filter((line) => line.startsWith("data:"))
140
+ .map((line) => line.slice(5).trimStart())
141
+ .join("\n");
142
+ if (data) {
143
+ const event = publicWorkbenchEvent(JSON.parse(data));
144
+ if (event) onEvent(event);
145
+ }
146
+ boundary = buffer.indexOf("\n\n");
147
+ }
148
+ if (buffer.length > 1_048_576)
149
+ throw new Error("事件流超出安全大小,请手动刷新");
150
+ }
151
+ } finally {
152
+ await reader.cancel().catch(() => undefined);
153
+ reader.releaseLock();
154
+ }
155
+ }
@@ -0,0 +1,120 @@
1
+ import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ const mocks = vi.hoisted(() => ({ request: vi.fn(), response: vi.fn() }));
3
+ vi.mock("./http", () => ({
4
+ apiRequest: mocks.request,
5
+ apiResponse: mocks.response,
6
+ }));
7
+ import {
8
+ cancelTestRun,
9
+ createTestRun,
10
+ createTestSession,
11
+ getTestRunResult,
12
+ getWorkbenchRecording,
13
+ streamTestRun,
14
+ } from "./agent-admin-api";
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ mocks.request.mockResolvedValue({});
18
+ });
19
+ describe("工作台正常治理 API 契约", () => {
20
+ test("创建真实Session/run并绑定幂等键,result/cancel带同一Session与Operation", async () => {
21
+ const controller = new AbortController();
22
+ await createTestSession(controller.signal);
23
+ await createTestRun(
24
+ "finance.query",
25
+ { question: "sum" },
26
+ "session_one",
27
+ controller.signal,
28
+ );
29
+ expect(mocks.request.mock.calls[0]?.[0]).toBe("/agent/admin/test-sessions");
30
+ const request = mocks.request.mock.calls[1]!;
31
+ expect(request[0]).toBe("/agent/runs");
32
+ expect(JSON.parse(request[1].body)).toEqual({
33
+ operationKey: "finance.query",
34
+ input: { question: "sum" },
35
+ sessionId: "session_one",
36
+ });
37
+ expect(request[1].headers["idempotency-key"]).toMatch(/^[0-9a-f-]{36}$/);
38
+ expect(request[1].signal).toBe(controller.signal);
39
+ await getTestRunResult(
40
+ "run_one",
41
+ "finance.query",
42
+ "session_one",
43
+ controller.signal,
44
+ );
45
+ await cancelTestRun(
46
+ "run_one",
47
+ "finance.query",
48
+ "session_one",
49
+ controller.signal,
50
+ );
51
+ expect(mocks.request.mock.calls[2]?.[0]).toBe(
52
+ "/agent/runs/run_one/result?operationKey=finance.query&sessionId=session_one",
53
+ );
54
+ expect(mocks.request.mock.calls[3]?.[0]).toContain(
55
+ "/cancel?operationKey=finance.query&sessionId=session_one",
56
+ );
57
+ expect(mocks.request.mock.calls[3]?.[1].signal).toBe(controller.signal);
58
+ });
59
+ test("历史读取只请求受控recording,不触发result或启动接口", async () => {
60
+ await getWorkbenchRecording("business/session", "business");
61
+ expect(mocks.request).toHaveBeenCalledTimes(1);
62
+ expect(mocks.request.mock.calls[0]?.[0]).toBe(
63
+ "/agent/admin/session-recordings/business%2Fsession?source=business",
64
+ );
65
+ });
66
+ test("SSE跨chunk CRLF与UTF8、同chunk多frame不会漏事件或保留私有参数", async () => {
67
+ const encoder = new TextEncoder();
68
+ const frames = [
69
+ 'data: {"sequence":1,"type":"tool.requested","summary":"位置工具","arguments":{"token":"hidden"}}\r\n\r\n',
70
+ 'data: {"sequence":2,"type":"run.completed","summary":"已完成"}\r\n\r\n',
71
+ ].join("");
72
+ const bytes = encoder.encode(frames);
73
+ const split =
74
+ frames.indexOf("\r\n") +
75
+ encoder.encode("位置工具").length -
76
+ "位置工具".length +
77
+ 1;
78
+ mocks.response.mockResolvedValue(
79
+ new Response(
80
+ new ReadableStream({
81
+ start(controller) {
82
+ controller.enqueue(bytes.slice(0, split));
83
+ controller.enqueue(bytes.slice(split));
84
+ controller.close();
85
+ },
86
+ }),
87
+ ),
88
+ );
89
+ const events: unknown[] = [];
90
+ await streamTestRun(
91
+ "run_1",
92
+ "query",
93
+ "session_1",
94
+ (event) => events.push(event),
95
+ new AbortController().signal,
96
+ );
97
+ expect(events).toHaveLength(2);
98
+ expect(JSON.stringify(events)).toContain("位置工具");
99
+ expect(JSON.stringify(events)).not.toContain("hidden");
100
+ });
101
+ test("单chunk的多个合法frame总大小超过1MB仍逐条处理", async () => {
102
+ const data = Array.from(
103
+ { length: 100 },
104
+ (_, index) =>
105
+ `data: ${JSON.stringify({ sequence: index + 1, type: "output.delta", summary: "a".repeat(11000) })}\n\n`,
106
+ ).join("");
107
+ mocks.response.mockResolvedValue(new Response(data));
108
+ let count = 0;
109
+ await streamTestRun(
110
+ "run_1",
111
+ "query",
112
+ "session_1",
113
+ () => {
114
+ count += 1;
115
+ },
116
+ new AbortController().signal,
117
+ );
118
+ expect(count).toBe(100);
119
+ });
120
+ });
@@ -0,0 +1,60 @@
1
+ import type { AgentWorkbenchEventV1 } from "@southwind-ai/shared";
2
+ /** Defense in depth: discard fields outside the server's public event contract. */
3
+ export function publicWorkbenchEvent(
4
+ value: unknown,
5
+ ): AgentWorkbenchEventV1 | undefined {
6
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
7
+ const raw = value as Record<string, unknown>;
8
+ if (
9
+ !Number.isSafeInteger(raw.sequence) ||
10
+ Number(raw.sequence) < 0 ||
11
+ typeof raw.type !== "string"
12
+ )
13
+ return;
14
+ const summary = typeof raw.summary === "string" ? raw.summary : raw.type;
15
+ const strings = [
16
+ "occurredAt",
17
+ "runId",
18
+ "turnId",
19
+ "stepId",
20
+ "modelAttemptId",
21
+ "toolCallId",
22
+ "toolKey",
23
+ "workItemId",
24
+ "artifactId",
25
+ "workflowRunId",
26
+ "nodeRunId",
27
+ "outcome",
28
+ ] as const;
29
+ const refs = Object.fromEntries(
30
+ strings.flatMap((key) =>
31
+ typeof raw[key] === "string" ? [[key, raw[key]]] : [],
32
+ ),
33
+ );
34
+ const usage =
35
+ raw.usage && typeof raw.usage === "object" && !Array.isArray(raw.usage)
36
+ ? (raw.usage as Record<string, unknown>)
37
+ : undefined;
38
+ const measured = usage
39
+ ? Object.fromEntries(
40
+ ["inputTokens", "outputTokens", "totalTokens", "costMicros"].flatMap(
41
+ (key) =>
42
+ typeof usage[key] === "number" &&
43
+ Number.isFinite(usage[key]) &&
44
+ Number(usage[key]) >= 0
45
+ ? [[key, usage[key]]]
46
+ : [],
47
+ ),
48
+ )
49
+ : undefined;
50
+ return {
51
+ sequence: Number(raw.sequence),
52
+ type: raw.type,
53
+ summary,
54
+ ...refs,
55
+ ...(Number.isSafeInteger(raw.attemptSequence)
56
+ ? { attemptSequence: Number(raw.attemptSequence) }
57
+ : {}),
58
+ ...(measured ? { usage: measured } : {}),
59
+ };
60
+ }
@@ -462,3 +462,7 @@ Compose、`service_adk_debug` 或活跃 `ADK_DEBUG_*` 配置。
462
462
  - [DeepSeek Harness Architecture](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md)
463
463
  - [Vue Flow](https://vueflow.dev/):Vue 3 Workflow 画布候选;采用前仍需完成依赖评估与固定版本。
464
464
  - [PageAgent](https://github.com/alibaba/page-agent):页面理解与浏览器动作复用边界。
465
+
466
+ ## 管理端调试与业务回放(2026-10-07)
467
+
468
+ 动态工具目录与统一调试工作台沿用本 Harness 治理边界;业务会话可提供真实 Run/Workflow 引用,父子 Session 回放沿用 AH-1 checkpoint/lineage。历史只读取已有记录,缺少引用或超出有界窗口时显示 partial/missing。测试运行新建独立 Session,不在历史回放中启动模型或工具。实现与发布边界见[工作台技术实现](./agent-admin-workbench-implementation.md),本节不将尚未发布的隔离执行与 Trace 扩展视为已交付。
@@ -1,6 +1,7 @@
1
1
  # 结构化运行日志技术实现
2
2
 
3
- 状态:SDK、平台适配与利剑四条链的本地定向联调已实现;正式发布/升级及集中查询/告警平台未验收。
3
+ 状态:SDK 与 E1 已随 Windy `0.8.1` 正式发布,利剑完成官方更新和正式 SDK 四条链定向联调;
4
+ 完整隔离执行、真实 Provider、生产采集留存及集中查询/告警平台未验收。
4
5
  对应业务方案见
5
6
  [日志治理方案](./log-governance-plan.md)。
6
7
 
@@ -75,5 +76,21 @@
75
76
  容器依赖层安装和已有静态失败在利剑最终文档分别记录。
76
77
  候选 SDK 用真实本地归档消费,没有改 `.windy` 元数据或手改版本号冒充正式升级。
77
78
 
78
- 本轮完成日志契约与四条真实适配链的本地定向联调;未完成真实 Provider/完整 Agent 部署、正式
79
- Windy 发布和 `windy update` 事务、生产采集留存、集中查询或告警验收。这些不能由定向检查替代。
79
+ ## 正式发布与利剑升级
80
+
81
+ 2026-10-01 核验 `create-windy@0.8.1`、`@southwind-ai/shared@0.3.1` 已在 npm 发布,
82
+ `create-windy-v0.8.1` tag 指向 `066d8018c0ec0c82fbf01d26013b3b303aaa9c22`。
83
+ [发布记录](https://github.com/RayJason/windy-platform/releases/tag/create-windy-v0.8.1) 已附
84
+ candidate/published evidence、distribution manifest 与 checksum;两次发布核验均成功。
85
+ 利剑官方更新账本绑定同一 SHA 和 `0.8.1`,当前使用正常的
86
+ `@southwind-ai/shared/structured-log`,已删除候选别名及归档。详细更新及验收记录在利剑
87
+ `docs/operations/windy-logging-integration.md`。
88
+
89
+ 未完成 E2—E4 集成发布、真实 Provider/完整 Agent 部署、生产采集留存、集中查询或告警验收。
90
+ 日志 SDK 正式接入不能当作完整 Agent 能力已经交付的证据。
91
+
92
+ 利剑合并验收另发现旧 Bootstrap 借用 Uvicorn Handler 后再追加 SDK 会形成双出口。
93
+ `configure_agent_logging()` 已回馈上游:保留唯一现存结构化 Handler,移除包级其它 Handler 引用,
94
+ 不关闭与 Uvicorn 共享的 Handler;重复初始化保留原实例和 Stream。5 项适配定向检查通过。
95
+ 该修复是 `0.8.1` 发布后的源码补丁,没有改写发布版本;利剑同时修复旧 Bootstrap 调用点,并用
96
+ 真实 Uvicorn 配置先于 ASGI 导入的回归检查证明单条 JSON 输出及独立访问日志仍有效。
@@ -1,7 +1,7 @@
1
1
  # 日志治理方案
2
2
 
3
- 状态:运行日志 SDK、平台适配及利剑四条链的本地定向联调已实现;正式发布、下游升级与部署留存
4
- 另行验收。集中管理界面尚未实施。
3
+ 状态:运行日志 SDK 和平台适配随 Windy `0.8.1` 正式发布,利剑已通过官方更新接入并完成四条链
4
+ 定向联调。部署留存与真实 Provider 另行验收;集中管理界面尚未实施。
5
5
 
6
6
  ## 业务需求先行
7
7
 
@@ -52,6 +52,24 @@ Provider 调用前会执行:
52
52
  Tool 调用继续由 Windy Server 执行。Agent Server 只收到 opaque Execution Grant;每次 Tool
53
53
  都重新校验当前权限、Scope、审批和 receipt,不把 Run 创建时的权限快照当长期授权。
54
54
 
55
+ ### Tool 与结构化业务结果
56
+
57
+ 需要先查询业务数据再返回结构化结果的 Operation,同时保留真实 function calling 与最终
58
+ JSON Schema 验证。为兼容同时发送 `tools` 和 `response_format` 时只生成正文的 Provider,
59
+ 存在 Tool 时请求只发送 `tools`,把经过授权和 DLP 的输出 Schema 及最终 JSON 指导合并到
60
+ 首条系统指令;没有 Tool 时继续发送严格 `response_format: json_schema`。这是默认序列化
61
+ 规则,无新增配置。正文中的工具名称或伪造调用不执行,只有协议中的 `tool_calls` 执行。
62
+
63
+ 合并不增加消息、移动 Context 来源序号或放宽 Guard。治理后的同一消息用于 Context 提交、
64
+ 输入预算和实际出网请求;Schema 指导也计入 Provider Context 与保守 Token 预算、出网审计
65
+ 字节数。`maxInputBytes` 继续约束原始输入和 Artifact,原有输出预算与有界 Tool 轮次继续生效。
66
+ 最终回复仍必须通过严格 Schema 验证,
67
+ 不接受 Markdown 围栏或额外字段;Provider 未生成合规结果时返回 `OUTPUT_VALIDATION_FAILED`。
68
+
69
+ Context 的首条指令摘要与 `requestDigest` 覆盖完整、脱敏后的 Schema 指导。兼容请求没有
70
+ `response_format`,因此 `outputSchemaDigest` 是实际空字段的摘要;它不能单独用来恢复
71
+ 输出 Schema。首条指令的来源与版本规则保持不变,业务来源仍按原始序号追踪。
72
+
55
73
  ## 配置
56
74
 
57
75
  选择 `system.agent` 的项目在本机 `.env` 配置:
@@ -57,6 +57,15 @@ function validateOperation(
57
57
  operation: ModuleAiOperationDefinition,
58
58
  catalogs: AiOperationCatalogs,
59
59
  ): void {
60
+ if (
61
+ operation.visibility !== undefined &&
62
+ operation.visibility !== "public" &&
63
+ operation.visibility !== "internal"
64
+ ) {
65
+ throw new Error(
66
+ `AI Operation ${operation.key} visibility 必须为 public 或 internal`,
67
+ );
68
+ }
60
69
  const strings = [
61
70
  operation.key,
62
71
  operation.featureKey,