create-windy 0.4.0 → 0.5.0
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.
- package/dist/cli.js +263 -121
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
- package/template/apps/agent-server/adk_web_agents/services.py +25 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
- package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
- package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
- package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
- package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
- package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
- package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
- package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
- package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
- package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
- package/template/apps/agent-server/tests/test_report_store.py +286 -0
- package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
- package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
- package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
- package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
- package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
- package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
- package/template/apps/server/src/agent/debug-session-id.ts +37 -0
- package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
- package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
- package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
- package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
- package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
- package/template/apps/server/src/agent/run-authorization.ts +32 -1
- package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +5 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
- package/template/apps/server/src/agent/run-facade.test.ts +78 -0
- package/template/apps/server/src/agent/run-facade.ts +33 -3
- package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
- package/template/apps/server/src/agent/run-input-verification.ts +41 -0
- package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
- package/template/apps/server/src/agent/tool-host.test.ts +1 -0
- package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
- package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
- package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
- package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
- package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
- package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
- package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
- package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
- package/template/apps/server/src/license/runtime-service.test.ts +6 -0
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
- package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
- package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
- package/template/apps/server/src/module-host/initialize.ts +6 -197
- package/template/apps/server/src/module-host/request-context.ts +1 -0
- package/template/apps/server/src/module-host/scoped-host.ts +214 -0
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
- package/template/apps/server/src/runtime-adk-debug.ts +8 -2
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
- package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
- package/template/apps/server/src/settings/watermark-routes.ts +29 -1
- package/template/apps/server/src/system/operations.test.ts +54 -0
- package/template/apps/server/src/system/operations.ts +23 -1
- package/template/apps/server/src/system/seed.test.ts +4 -0
- package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
- package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
- package/template/apps/web/src/composables/watermark-settings.ts +40 -11
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
- package/template/apps/web/src/layout/AppSidebar.vue +6 -1
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
- package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
- package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
- package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
- package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
- package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
- package/template/apps/web/src/router/error-navigation.ts +22 -0
- package/template/apps/web/src/router/index.ts +12 -23
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
- package/template/apps/web/src/services/operations-api.ts +28 -0
- package/template/docker-compose.agent-debug.yml +1 -1
- package/template/docs/architecture/ai-runtime.md +2 -1
- package/template/docs/architecture/module-host.md +3 -0
- package/template/docs/platform/agent-run-attestation.md +65 -0
- package/template/docs/platform/agent-runtime-observability.md +135 -0
- package/template/docs/platform/agent-runtime.md +47 -3
- package/template/docs/platform/operations-overview.md +13 -0
- package/template/docs/platform/platform-authentication.md +4 -3
- package/template/docs/platform/platform-shell-settings.md +15 -10
- package/template/docs/platform/web-system-crud.md +1 -1
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
- package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
- package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
- package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
- package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
- package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
- package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
- package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +114 -0
- package/template/packages/database/src/schema/platform-settings.ts +3 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +10 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-api-control-plane.ts +17 -1
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +8 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
- package/template/packages/modules/src/system-module-catalog.ts +15 -4
- package/template/packages/modules/src/system-modules-access.test.ts +69 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
- package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
- package/template/packages/server-sdk/src/module-host.ts +5 -0
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +134 -0
- package/template/packages/shared/src/platform-settings.ts +17 -0
- package/template/scripts/lib/postgres-regression.ts +201 -0
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ from google.adk.agents import LlmAgent
|
|
|
8
8
|
from google.adk.models.base_llm import BaseLlm
|
|
9
9
|
from google.adk.models.llm_request import LlmRequest
|
|
10
10
|
from google.adk.models.llm_response import LlmResponse
|
|
11
|
+
from google.adk.tools.tool_context import ToolContext
|
|
11
12
|
from google.genai import types
|
|
12
13
|
|
|
13
14
|
from southwind_agent_server.adk_debug import (
|
|
@@ -15,13 +16,20 @@ from southwind_agent_server.adk_debug import (
|
|
|
15
16
|
TextOperationDebugConfig,
|
|
16
17
|
load_text_operation_debug_config,
|
|
17
18
|
)
|
|
19
|
+
from southwind_agent_server.adk_debug_sessions import (
|
|
20
|
+
IMPORTED_SESSION_STATE_KEY,
|
|
21
|
+
ImportedDebugSessionReadOnlyError,
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
TOOL_NAME = "run_canonical_operation"
|
|
20
25
|
PROJECT_OPERATION = load_text_operation_debug_config()
|
|
21
26
|
|
|
22
27
|
|
|
23
|
-
async def run_canonical_operation(
|
|
28
|
+
async def run_canonical_operation(
|
|
29
|
+
question: str, tool_context: ToolContext
|
|
30
|
+
) -> dict[str, object]:
|
|
24
31
|
"""Run the project-selected text Operation through canonical platform APIs."""
|
|
32
|
+
ensure_debug_session_writable(tool_context.state.get(IMPORTED_SESSION_STATE_KEY))
|
|
25
33
|
if PROJECT_OPERATION is None:
|
|
26
34
|
raise RuntimeError("项目尚未选择可调试的文本 Operation")
|
|
27
35
|
return await CanonicalRunDebugClient.from_environment(
|
|
@@ -29,6 +37,13 @@ async def run_canonical_operation(question: str) -> dict[str, object]:
|
|
|
29
37
|
).run(question)
|
|
30
38
|
|
|
31
39
|
|
|
40
|
+
def ensure_debug_session_writable(imported_marker: object) -> None:
|
|
41
|
+
if imported_marker is True:
|
|
42
|
+
raise ImportedDebugSessionReadOnlyError(
|
|
43
|
+
"导入的业务会话是实时只读投影;请新建本地会话继续提问"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
32
47
|
class CanonicalDebugLlm(BaseLlm):
|
|
33
48
|
"""ADK dispatcher; real model and Tool execution stay in canonical Run."""
|
|
34
49
|
|
|
@@ -42,7 +57,8 @@ class CanonicalDebugLlm(BaseLlm):
|
|
|
42
57
|
stream: bool = False,
|
|
43
58
|
) -> AsyncGenerator[LlmResponse, None]:
|
|
44
59
|
del stream
|
|
45
|
-
|
|
60
|
+
question, user_turn_index = latest_user_turn(llm_request)
|
|
61
|
+
tool_response = latest_tool_response(llm_request, user_turn_index)
|
|
46
62
|
if tool_response is not None:
|
|
47
63
|
yield text_response(render_result(tool_response))
|
|
48
64
|
return
|
|
@@ -52,7 +68,6 @@ class CanonicalDebugLlm(BaseLlm):
|
|
|
52
68
|
"选择真实文本 Operation。"
|
|
53
69
|
)
|
|
54
70
|
return
|
|
55
|
-
question = latest_user_text(llm_request)
|
|
56
71
|
if not question:
|
|
57
72
|
yield text_response("请输入要交给真实 Agent Runtime 调试的业务问题。")
|
|
58
73
|
return
|
|
@@ -72,18 +87,23 @@ class CanonicalDebugLlm(BaseLlm):
|
|
|
72
87
|
)
|
|
73
88
|
|
|
74
89
|
|
|
75
|
-
def
|
|
76
|
-
for
|
|
90
|
+
def latest_user_turn(request: LlmRequest) -> tuple[str, int]:
|
|
91
|
+
for index in range(len(request.contents) - 1, -1, -1):
|
|
92
|
+
content = request.contents[index]
|
|
77
93
|
if content.role != "user":
|
|
78
94
|
continue
|
|
79
95
|
text = "".join(part.text or "" for part in content.parts or [])
|
|
80
96
|
if text.strip():
|
|
81
|
-
return text.strip()
|
|
82
|
-
return ""
|
|
97
|
+
return text.strip(), index
|
|
98
|
+
return "", -1
|
|
83
99
|
|
|
84
100
|
|
|
85
|
-
def latest_tool_response(
|
|
86
|
-
|
|
101
|
+
def latest_tool_response(
|
|
102
|
+
request: LlmRequest, user_turn_index: int
|
|
103
|
+
) -> dict[str, object] | None:
|
|
104
|
+
if user_turn_index < 0:
|
|
105
|
+
return None
|
|
106
|
+
for content in reversed(request.contents[user_turn_index + 1 :]):
|
|
87
107
|
for part in content.parts or []:
|
|
88
108
|
response = part.function_response
|
|
89
109
|
if response and response.name == TOOL_NAME:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from google.adk.cli.service_registry import get_service_registry
|
|
6
|
+
|
|
7
|
+
from southwind_agent_server.adk_debug_sessions import (
|
|
8
|
+
WindyDebugSessionClientV1,
|
|
9
|
+
WindyDebugSessionServiceV1,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
SESSION_SERVICE_URI = "windy-debug://v1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_windy_debug_session_service(
|
|
16
|
+
uri: str, **_kwargs: Any
|
|
17
|
+
) -> WindyDebugSessionServiceV1:
|
|
18
|
+
if uri != SESSION_SERVICE_URI:
|
|
19
|
+
raise ValueError("Windy ADK debug session URI 版本不受支持")
|
|
20
|
+
return WindyDebugSessionServiceV1(WindyDebugSessionClientV1.from_environment())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
get_service_registry().register_session_service(
|
|
24
|
+
"windy-debug", create_windy_debug_session_service
|
|
25
|
+
)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import AsyncIterator
|
|
5
|
+
from typing import cast
|
|
5
6
|
|
|
6
7
|
from google.adk.agents import LlmAgent
|
|
7
8
|
from google.adk.agents.run_config import RunConfig, StreamingMode
|
|
@@ -13,7 +14,6 @@ from google.genai import types
|
|
|
13
14
|
from .adk_failure import AdkFailureState
|
|
14
15
|
from .adk_model import GovernedOpenAiLlm
|
|
15
16
|
from .adk_tools import ApprovalSignalState, GovernedTool
|
|
16
|
-
from .contracts import JsonValue
|
|
17
17
|
from .engine import (
|
|
18
18
|
AgentEngineAdapter,
|
|
19
19
|
EngineCompleted,
|
|
@@ -26,9 +26,11 @@ from .engine import (
|
|
|
26
26
|
UsageReported,
|
|
27
27
|
)
|
|
28
28
|
from .multimodal import final_output
|
|
29
|
-
from .
|
|
29
|
+
from .openai_engine_validation import cancelled, validate_runtime_request
|
|
30
30
|
from .openai_provider import OpenAiCompatibleProvider
|
|
31
31
|
from .provider_governance import ProviderRequestGovernance
|
|
32
|
+
from .report_store import AgentRuntimeReportStore
|
|
33
|
+
from .report_types import CacheStatus, UsageFacts, UsageSource
|
|
32
34
|
from .tool_host import ToolHostClient, canonical_tool_call_id
|
|
33
35
|
|
|
34
36
|
|
|
@@ -40,10 +42,12 @@ class AdkEngineAdapter(AgentEngineAdapter):
|
|
|
40
42
|
provider: OpenAiCompatibleProvider | None = None,
|
|
41
43
|
tool_host: ToolHostClient | None = None,
|
|
42
44
|
governance: ProviderRequestGovernance | None = None,
|
|
45
|
+
report_store: AgentRuntimeReportStore | None = None,
|
|
43
46
|
) -> None:
|
|
44
47
|
self._provider = provider
|
|
45
48
|
self._tool_host = tool_host
|
|
46
49
|
self._governance = governance
|
|
50
|
+
self._report_store = report_store
|
|
47
51
|
self._cancellations: dict[str, asyncio.Event] = {}
|
|
48
52
|
|
|
49
53
|
@property
|
|
@@ -95,6 +99,7 @@ class AdkEngineAdapter(AgentEngineAdapter):
|
|
|
95
99
|
governance,
|
|
96
100
|
cancellation,
|
|
97
101
|
failures,
|
|
102
|
+
self._report_store,
|
|
98
103
|
),
|
|
99
104
|
instruction=request.instructions,
|
|
100
105
|
tools=[
|
|
@@ -241,21 +246,45 @@ async def signals_for_event(
|
|
|
241
246
|
if part.text and not part.thought:
|
|
242
247
|
yield OutputDelta(part.text)
|
|
243
248
|
usage = southwind_usage(event)
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
attempt_sequence = southwind_attempt_sequence(event)
|
|
250
|
+
if usage is not None and attempt_sequence is not None:
|
|
251
|
+
yield UsageReported(attempt_sequence, usage)
|
|
246
252
|
|
|
247
253
|
|
|
248
|
-
def southwind_usage(event: Event) ->
|
|
254
|
+
def southwind_usage(event: Event) -> UsageFacts | None:
|
|
249
255
|
metadata = event.custom_metadata or {}
|
|
250
256
|
value = metadata.get("southwindUsage")
|
|
251
257
|
if not isinstance(value, dict):
|
|
252
258
|
return None
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
source = value.get("source")
|
|
260
|
+
cache_status = value.get("cacheStatus")
|
|
261
|
+
if source not in {"provider-reported", "estimated", "unknown"}:
|
|
262
|
+
return None
|
|
263
|
+
if cache_status not in {"hit", "miss", "unknown"}:
|
|
264
|
+
return None
|
|
265
|
+
return UsageFacts(
|
|
266
|
+
source=cast(UsageSource, source),
|
|
267
|
+
cache_status=cast(CacheStatus, cache_status),
|
|
268
|
+
input_tokens=measurement(value.get("inputTokens")),
|
|
269
|
+
output_tokens=measurement(value.get("outputTokens")),
|
|
270
|
+
cached_input_tokens=measurement(value.get("cachedInputTokens")),
|
|
271
|
+
reasoning_tokens=measurement(value.get("reasoningTokens")),
|
|
272
|
+
total_tokens=measurement(value.get("totalTokens")),
|
|
273
|
+
first_token_latency_ms=measurement(value.get("firstTokenLatencyMs")),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def southwind_attempt_sequence(event: Event) -> int | None:
|
|
278
|
+
value = (event.custom_metadata or {}).get("southwindAttemptSequence")
|
|
279
|
+
return value if isinstance(value, int) and value > 0 else None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def measurement(value: object) -> int | None:
|
|
283
|
+
return (
|
|
284
|
+
value
|
|
285
|
+
if isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
|
286
|
+
else None
|
|
287
|
+
)
|
|
259
288
|
|
|
260
289
|
|
|
261
290
|
async def merge_adk_events(
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Literal, Protocol, cast, override
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from google.adk.events.event import Event
|
|
11
|
+
from google.adk.sessions.base_session_service import (
|
|
12
|
+
BaseSessionService,
|
|
13
|
+
GetSessionConfig,
|
|
14
|
+
ListSessionsResponse,
|
|
15
|
+
)
|
|
16
|
+
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
|
17
|
+
from google.adk.sessions.session import Session
|
|
18
|
+
from google.genai import types
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
20
|
+
|
|
21
|
+
from .config import normalize_base_url
|
|
22
|
+
|
|
23
|
+
CANONICAL_DEBUG_APP = "canonical_debug"
|
|
24
|
+
CANONICAL_DEBUG_USER = "user"
|
|
25
|
+
IMPORTED_SESSION_ID_PREFIX = "wdbg1_"
|
|
26
|
+
IMPORTED_SESSION_ID_PATTERN = re.compile(r"^wdbg1_[A-Za-z0-9_-]{43}$")
|
|
27
|
+
IMPORTED_SESSION_STATE_KEY = "__windy_debug_session_v1__"
|
|
28
|
+
MAX_MESSAGES = 64
|
|
29
|
+
MAX_TOTAL_MESSAGE_CHARS = 128_000
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ImportedDebugSessionReadOnlyError(RuntimeError):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _StrictModel(BaseModel):
|
|
38
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DebugSessionSummaryV1(_StrictModel):
|
|
42
|
+
id: str = Field(pattern=r"^wdbg1_[A-Za-z0-9_-]{43}$")
|
|
43
|
+
title: str = Field(min_length=1, max_length=200)
|
|
44
|
+
scopeLabel: str | None = Field(default=None, min_length=1, max_length=120)
|
|
45
|
+
updatedAt: str = Field(min_length=1, max_length=40)
|
|
46
|
+
revision: str = Field(min_length=1, max_length=200)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DebugSessionMessageV1(_StrictModel):
|
|
50
|
+
sequence: int = Field(ge=0)
|
|
51
|
+
role: Literal["user", "assistant"]
|
|
52
|
+
content: str = Field(min_length=1, max_length=16_000)
|
|
53
|
+
createdAt: str = Field(min_length=1, max_length=40)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DebugSessionDetailV1(DebugSessionSummaryV1):
|
|
57
|
+
messages: list[DebugSessionMessageV1] = Field(max_length=MAX_MESSAGES)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DebugSessionListEnvelopeV1(_StrictModel):
|
|
61
|
+
schemaVersion: Literal[1]
|
|
62
|
+
sessions: list[DebugSessionSummaryV1]
|
|
63
|
+
truncated: bool
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DebugSessionDetailEnvelopeV1(_StrictModel):
|
|
67
|
+
schemaVersion: Literal[1]
|
|
68
|
+
session: DebugSessionDetailV1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class DebugSessionClientPortV1(Protocol):
|
|
72
|
+
async def list(self) -> DebugSessionListEnvelopeV1: ...
|
|
73
|
+
|
|
74
|
+
async def get(self, session_id: str) -> DebugSessionDetailV1 | None: ...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class WindyDebugSessionClientV1:
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
base_url: str,
|
|
81
|
+
bearer_token: str,
|
|
82
|
+
*,
|
|
83
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
self._base_url = normalize_base_url(base_url)
|
|
86
|
+
self._bearer_token = bearer_token.strip()
|
|
87
|
+
self._transport = transport
|
|
88
|
+
if not self._bearer_token:
|
|
89
|
+
raise RuntimeError("ADK_DEBUG_PLATFORM_TOKEN 未配置,拒绝读取调试会话")
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_environment(cls) -> WindyDebugSessionClientV1:
|
|
93
|
+
return cls(
|
|
94
|
+
os.environ.get("ADK_DEBUG_PLATFORM_URL", "http://server:9747"),
|
|
95
|
+
os.environ.get("ADK_DEBUG_PLATFORM_TOKEN", ""),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def list(self) -> DebugSessionListEnvelopeV1:
|
|
99
|
+
value = await self._get("api/agent/debug/sessions/v1")
|
|
100
|
+
try:
|
|
101
|
+
envelope = DebugSessionListEnvelopeV1.model_validate(value)
|
|
102
|
+
for session in envelope.sessions:
|
|
103
|
+
parse_timestamp(session.updatedAt)
|
|
104
|
+
return envelope
|
|
105
|
+
except (ValidationError, ValueError) as error:
|
|
106
|
+
raise RuntimeError("平台调试会话列表响应无效") from error
|
|
107
|
+
|
|
108
|
+
async def get(self, session_id: str) -> DebugSessionDetailV1 | None:
|
|
109
|
+
if not IMPORTED_SESSION_ID_PATTERN.fullmatch(session_id):
|
|
110
|
+
raise RuntimeError("平台调试会话 ID 无效")
|
|
111
|
+
value = await self._get(
|
|
112
|
+
f"api/agent/debug/sessions/v1/{session_id}",
|
|
113
|
+
allow_not_found=True,
|
|
114
|
+
)
|
|
115
|
+
if value is None:
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
session = DebugSessionDetailEnvelopeV1.model_validate(value).session
|
|
119
|
+
validate_detail(session)
|
|
120
|
+
return session
|
|
121
|
+
except (ValidationError, ValueError) as error:
|
|
122
|
+
raise RuntimeError("平台调试会话详情响应无效") from error
|
|
123
|
+
|
|
124
|
+
async def _get(self, path: str, *, allow_not_found: bool = False) -> object | None:
|
|
125
|
+
headers = {
|
|
126
|
+
"authorization": f"Bearer {self._bearer_token}",
|
|
127
|
+
"user-agent": "Windy-ADK-Web-Dev",
|
|
128
|
+
}
|
|
129
|
+
async with httpx.AsyncClient(
|
|
130
|
+
base_url=f"{self._base_url}/",
|
|
131
|
+
headers=headers,
|
|
132
|
+
timeout=httpx.Timeout(15),
|
|
133
|
+
transport=self._transport,
|
|
134
|
+
) as client:
|
|
135
|
+
response = await client.get(path)
|
|
136
|
+
if allow_not_found and response.status_code == 404:
|
|
137
|
+
return None
|
|
138
|
+
if not response.is_success:
|
|
139
|
+
raise RuntimeError("平台调试会话读取失败")
|
|
140
|
+
try:
|
|
141
|
+
return cast(object, response.json())
|
|
142
|
+
except ValueError as error:
|
|
143
|
+
raise RuntimeError("平台调试会话响应不是有效 JSON") from error
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class WindyDebugSessionServiceV1(BaseSessionService):
|
|
147
|
+
"""Keep normal ADK sessions local and overlay live read-only projections."""
|
|
148
|
+
|
|
149
|
+
def __init__(
|
|
150
|
+
self,
|
|
151
|
+
client: DebugSessionClientPortV1,
|
|
152
|
+
local: InMemorySessionService | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
self._client = client
|
|
155
|
+
self._local = (
|
|
156
|
+
local if local is not None else InMemorySessionService() # type: ignore[no-untyped-call]
|
|
157
|
+
)
|
|
158
|
+
self._deleted_imports: set[str] = set()
|
|
159
|
+
|
|
160
|
+
@override
|
|
161
|
+
async def create_session(
|
|
162
|
+
self,
|
|
163
|
+
*,
|
|
164
|
+
app_name: str,
|
|
165
|
+
user_id: str,
|
|
166
|
+
state: dict[str, object] | None = None,
|
|
167
|
+
session_id: str | None = None,
|
|
168
|
+
) -> Session:
|
|
169
|
+
if session_id and session_id.startswith(IMPORTED_SESSION_ID_PREFIX):
|
|
170
|
+
raise ImportedDebugSessionReadOnlyError("导入的业务会话 ID 为只读保留值")
|
|
171
|
+
return await self._local.create_session(
|
|
172
|
+
app_name=app_name,
|
|
173
|
+
user_id=user_id,
|
|
174
|
+
state=state,
|
|
175
|
+
session_id=session_id,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@override
|
|
179
|
+
async def get_session(
|
|
180
|
+
self,
|
|
181
|
+
*,
|
|
182
|
+
app_name: str,
|
|
183
|
+
user_id: str,
|
|
184
|
+
session_id: str,
|
|
185
|
+
config: GetSessionConfig | None = None,
|
|
186
|
+
) -> Session | None:
|
|
187
|
+
if not self._is_imported_request(app_name, user_id, session_id):
|
|
188
|
+
return await self._local.get_session(
|
|
189
|
+
app_name=app_name,
|
|
190
|
+
user_id=user_id,
|
|
191
|
+
session_id=session_id,
|
|
192
|
+
config=config,
|
|
193
|
+
)
|
|
194
|
+
if session_id in self._deleted_imports:
|
|
195
|
+
return None
|
|
196
|
+
detail = await self._client.get(session_id)
|
|
197
|
+
return project_detail(detail, config) if detail else None
|
|
198
|
+
|
|
199
|
+
@override
|
|
200
|
+
async def list_sessions(
|
|
201
|
+
self, *, app_name: str, user_id: str | None = None
|
|
202
|
+
) -> ListSessionsResponse:
|
|
203
|
+
local = await self._local.list_sessions(app_name=app_name, user_id=user_id)
|
|
204
|
+
if app_name != CANONICAL_DEBUG_APP or user_id != CANONICAL_DEBUG_USER:
|
|
205
|
+
return local
|
|
206
|
+
try:
|
|
207
|
+
remote = await self._client.list()
|
|
208
|
+
except Exception:
|
|
209
|
+
# Discovery is best-effort; cancellation and other BaseException values
|
|
210
|
+
# still propagate instead of being mistaken for an offline platform.
|
|
211
|
+
logger.warning("平台调试会话不可用,仅返回本地 ADK 会话")
|
|
212
|
+
return local
|
|
213
|
+
imported = [
|
|
214
|
+
project_summary(session)
|
|
215
|
+
for session in remote.sessions
|
|
216
|
+
if session.id not in self._deleted_imports
|
|
217
|
+
]
|
|
218
|
+
return ListSessionsResponse(sessions=[*local.sessions, *imported])
|
|
219
|
+
|
|
220
|
+
@override
|
|
221
|
+
async def delete_session(
|
|
222
|
+
self, *, app_name: str, user_id: str, session_id: str
|
|
223
|
+
) -> None:
|
|
224
|
+
if self._is_imported_request(app_name, user_id, session_id):
|
|
225
|
+
self._deleted_imports.add(session_id)
|
|
226
|
+
return
|
|
227
|
+
await self._local.delete_session(
|
|
228
|
+
app_name=app_name, user_id=user_id, session_id=session_id
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
@override
|
|
232
|
+
async def append_event(self, session: Session, event: Event) -> Event:
|
|
233
|
+
if session.state.get(IMPORTED_SESSION_STATE_KEY) is True:
|
|
234
|
+
raise ImportedDebugSessionReadOnlyError(
|
|
235
|
+
"导入的业务会话是实时只读投影;请新建本地会话继续提问"
|
|
236
|
+
)
|
|
237
|
+
return await self._local.append_event(session=session, event=event)
|
|
238
|
+
|
|
239
|
+
@override
|
|
240
|
+
async def get_user_state(self, *, app_name: str, user_id: str) -> dict[str, object]:
|
|
241
|
+
return await self._local.get_user_state(app_name=app_name, user_id=user_id)
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _is_imported_request(app_name: str, user_id: str, session_id: str) -> bool:
|
|
245
|
+
return (
|
|
246
|
+
app_name == CANONICAL_DEBUG_APP
|
|
247
|
+
and user_id == CANONICAL_DEBUG_USER
|
|
248
|
+
and IMPORTED_SESSION_ID_PATTERN.fullmatch(session_id) is not None
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def project_summary(value: DebugSessionSummaryV1) -> Session:
|
|
253
|
+
return Session(
|
|
254
|
+
id=value.id,
|
|
255
|
+
app_name=CANONICAL_DEBUG_APP,
|
|
256
|
+
user_id=CANONICAL_DEBUG_USER,
|
|
257
|
+
state=imported_state(value),
|
|
258
|
+
events=[],
|
|
259
|
+
last_update_time=parse_timestamp(value.updatedAt),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def project_detail(
|
|
264
|
+
value: DebugSessionDetailV1,
|
|
265
|
+
config: GetSessionConfig | None,
|
|
266
|
+
) -> Session:
|
|
267
|
+
events = [project_message(value.id, message) for message in value.messages]
|
|
268
|
+
if config and config.after_timestamp is not None:
|
|
269
|
+
events = [
|
|
270
|
+
event for event in events if event.timestamp >= config.after_timestamp
|
|
271
|
+
]
|
|
272
|
+
if config and config.num_recent_events is not None:
|
|
273
|
+
events = (
|
|
274
|
+
[] if config.num_recent_events == 0 else events[-config.num_recent_events :]
|
|
275
|
+
)
|
|
276
|
+
return Session(
|
|
277
|
+
id=value.id,
|
|
278
|
+
app_name=CANONICAL_DEBUG_APP,
|
|
279
|
+
user_id=CANONICAL_DEBUG_USER,
|
|
280
|
+
state=imported_state(value),
|
|
281
|
+
events=events,
|
|
282
|
+
last_update_time=parse_timestamp(value.updatedAt),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def project_message(session_id: str, value: DebugSessionMessageV1) -> Event:
|
|
287
|
+
role = "user" if value.role == "user" else "model"
|
|
288
|
+
return Event(
|
|
289
|
+
id=f"{session_id}-{value.sequence}",
|
|
290
|
+
invocation_id=f"{session_id}-projection",
|
|
291
|
+
author="user" if value.role == "user" else CANONICAL_DEBUG_APP,
|
|
292
|
+
content=types.Content(role=role, parts=[types.Part(text=value.content)]),
|
|
293
|
+
timestamp=parse_timestamp(value.createdAt),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def imported_state(value: DebugSessionSummaryV1) -> dict[str, object]:
|
|
298
|
+
display_name = (
|
|
299
|
+
f"{value.title} · {value.scopeLabel}" if value.scopeLabel else value.title
|
|
300
|
+
)
|
|
301
|
+
return {
|
|
302
|
+
IMPORTED_SESSION_STATE_KEY: True,
|
|
303
|
+
"__session_metadata__": {"displayName": display_name},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def validate_detail(value: DebugSessionDetailV1) -> None:
|
|
308
|
+
parse_timestamp(value.updatedAt)
|
|
309
|
+
total_chars = 0
|
|
310
|
+
previous_sequence = -1
|
|
311
|
+
for message in value.messages:
|
|
312
|
+
parse_timestamp(message.createdAt)
|
|
313
|
+
if message.sequence <= previous_sequence:
|
|
314
|
+
raise ValueError("调试会话消息顺序无效")
|
|
315
|
+
previous_sequence = message.sequence
|
|
316
|
+
total_chars += len(message.content)
|
|
317
|
+
if total_chars > MAX_TOTAL_MESSAGE_CHARS:
|
|
318
|
+
raise ValueError("调试会话消息总量超过上限")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def parse_timestamp(value: str) -> float:
|
|
322
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
323
|
+
if parsed.tzinfo is None:
|
|
324
|
+
raise ValueError("调试会话时间必须包含时区")
|
|
325
|
+
return parsed.timestamp()
|