create-windy 0.6.18 → 0.6.20
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 +133 -54
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/README.md +5 -2
- package/template/apps/agent-server/README.md +6 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +23 -0
- package/template/apps/agent-server/src/southwind_agent_server/health_routes.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_compatibility.py +89 -0
- package/template/apps/agent-server/tests/test_runtime_compatibility.py +119 -0
- package/template/apps/server/src/agent/remote-runtime-compatibility.test.ts +147 -0
- package/template/apps/server/src/agent/remote-runtime.ts +66 -4
- package/template/apps/server/src/agent/runtime-bootstrap.ts +1 -0
- package/template/apps/web/src/components/common/FormDialog.vue +1 -0
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +1 -0
- package/template/apps/web/src/components/ui/dialog/DialogContent.vue +1 -1
- package/template/apps/web/src/components/ui/dialog/DialogScrollContent.vue +1 -1
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +1 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.vue +182 -0
- package/template/apps/web/src/pages/system/components/user-account/CreateDepartmentDialog.webtest.ts +93 -0
- package/template/apps/web/src/pages/system/components/user-account/RolePermissionSummary.vue +59 -0
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +21 -71
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.webtest.ts +71 -0
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.vue +48 -27
- package/template/apps/web/src/pages/system/components/user-account/UserRoleSelector.webtest.ts +30 -1
- package/template/docs/architecture/agent-runtime-compatibility.md +107 -0
- package/template/docs/architecture/ai-runtime.md +8 -15
- package/template/docs/platform/agent-runtime.md +5 -0
- package/template/packages/server-sdk/contracts/agent-runtime-compatibility/v1/offer.json +22 -0
- package/template/packages/server-sdk/index.ts +19 -0
- package/template/packages/server-sdk/package.json +2 -0
- package/template/packages/server-sdk/src/agent-runtime-compatibility.contract.test.ts +143 -0
- package/template/packages/server-sdk/src/agent-runtime-compatibility.public-api.test.ts +19 -0
- package/template/packages/server-sdk/src/agent-runtime-compatibility.ts +284 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
SCHEMA_VERSION = 1
|
|
8
|
+
PROTOCOL_VERSION = 1
|
|
9
|
+
RUNTIME_KIND = "agent"
|
|
10
|
+
COMPATIBILITY_PATH = "/api/agent/runtime/compatibility"
|
|
11
|
+
PROTOCOL_HEADER = "X-Windy-Agent-Runtime-Protocol"
|
|
12
|
+
OFFER_DIGEST_HEADER = "X-Windy-Agent-Runtime-Offer-Digest"
|
|
13
|
+
TOOL_CALLBACK_CONTRACT_VERSION = 1
|
|
14
|
+
TOOL_CALLBACK_AUTHENTICATION = "windy-internal-bearer-v1"
|
|
15
|
+
PROTOCOL_FEATURES = (
|
|
16
|
+
"canonical-sse-replay.v1",
|
|
17
|
+
"execution-grant-forwarding.v1",
|
|
18
|
+
"operation-release-attestation.v1",
|
|
19
|
+
"run-cancel.v1",
|
|
20
|
+
"run-lifecycle.v1",
|
|
21
|
+
"runtime-reporting.v1",
|
|
22
|
+
"tool-callback.v1",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_STABLE_VALUE = re.compile(r"^[a-z0-9][a-z0-9._-]{0,159}$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RuntimeCompatibilityError(ValueError):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def runtime_compatibility_offer(engine_id: str) -> dict[str, object]:
|
|
33
|
+
if not _STABLE_VALUE.fullmatch(engine_id):
|
|
34
|
+
raise RuntimeCompatibilityError("Agent Runtime engineId invalid")
|
|
35
|
+
protocol_versions = [PROTOCOL_VERSION]
|
|
36
|
+
protocol_features = sorted(PROTOCOL_FEATURES)
|
|
37
|
+
tool_callback: dict[str, object] = {
|
|
38
|
+
"contractVersion": TOOL_CALLBACK_CONTRACT_VERSION,
|
|
39
|
+
"authentication": TOOL_CALLBACK_AUTHENTICATION,
|
|
40
|
+
}
|
|
41
|
+
release_digest = _digest(
|
|
42
|
+
{
|
|
43
|
+
"runtimeKind": RUNTIME_KIND,
|
|
44
|
+
"engineId": engine_id,
|
|
45
|
+
"protocolVersions": protocol_versions,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
capabilities_digest = _digest(
|
|
49
|
+
{
|
|
50
|
+
"protocolFeatures": protocol_features,
|
|
51
|
+
"toolCallback": tool_callback,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
body: dict[str, object] = {
|
|
55
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
56
|
+
"runtimeKind": RUNTIME_KIND,
|
|
57
|
+
"engineId": engine_id,
|
|
58
|
+
"protocolVersions": protocol_versions,
|
|
59
|
+
"protocolFeatures": protocol_features,
|
|
60
|
+
"toolCallback": tool_callback,
|
|
61
|
+
"releaseDigest": release_digest,
|
|
62
|
+
"capabilitiesDigest": capabilities_digest,
|
|
63
|
+
}
|
|
64
|
+
return {**body, "offerDigest": _digest(body)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_runtime_compatibility_pin(
|
|
68
|
+
protocol_header: str | None,
|
|
69
|
+
offer_digest_header: str | None,
|
|
70
|
+
engine_id: str,
|
|
71
|
+
) -> None:
|
|
72
|
+
if protocol_header is None and offer_digest_header is None:
|
|
73
|
+
return
|
|
74
|
+
if protocol_header is None or offer_digest_header is None:
|
|
75
|
+
raise RuntimeCompatibilityError("Agent Runtime compatibility pin incomplete")
|
|
76
|
+
if protocol_header != str(PROTOCOL_VERSION):
|
|
77
|
+
raise RuntimeCompatibilityError("Agent Runtime protocol unsupported")
|
|
78
|
+
expected = runtime_compatibility_offer(engine_id)["offerDigest"]
|
|
79
|
+
if offer_digest_header != expected:
|
|
80
|
+
raise RuntimeCompatibilityError("Agent Runtime offer digest mismatch")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _digest(value: object) -> str:
|
|
84
|
+
encoded = json.dumps(
|
|
85
|
+
value,
|
|
86
|
+
ensure_ascii=False,
|
|
87
|
+
separators=(",", ":"),
|
|
88
|
+
).encode()
|
|
89
|
+
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from southwind_agent_server.app import create_app
|
|
12
|
+
from southwind_agent_server.deterministic import DeterministicEngineAdapter
|
|
13
|
+
from southwind_agent_server.runtime_compatibility import (
|
|
14
|
+
runtime_compatibility_offer,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
TOKEN = "runtime-compatibility-test-token"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
async def client() -> AsyncIterator[httpx.AsyncClient]:
|
|
22
|
+
app = create_app(DeterministicEngineAdapter(), internal_token=TOKEN)
|
|
23
|
+
async with httpx.AsyncClient(
|
|
24
|
+
transport=httpx.ASGITransport(app=app),
|
|
25
|
+
base_url="http://agent.test",
|
|
26
|
+
) as value:
|
|
27
|
+
yield value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def test_compatibility_offer_is_authenticated_and_matches_public_fixture(
|
|
31
|
+
client: httpx.AsyncClient,
|
|
32
|
+
) -> None:
|
|
33
|
+
denied = await client.get("/api/agent/runtime/compatibility")
|
|
34
|
+
allowed = await client.get(
|
|
35
|
+
"/api/agent/runtime/compatibility",
|
|
36
|
+
headers={"Authorization": f"Bearer {TOKEN}"},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert denied.status_code == 403
|
|
40
|
+
assert allowed.status_code == 200
|
|
41
|
+
assert allowed.json() == public_fixture()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def test_start_accepts_legacy_without_pin(
|
|
45
|
+
client: httpx.AsyncClient,
|
|
46
|
+
) -> None:
|
|
47
|
+
legacy = await start(client, "legacy-pin")
|
|
48
|
+
|
|
49
|
+
assert legacy.status_code == 200
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def test_start_accepts_complete_matching_pin(
|
|
53
|
+
client: httpx.AsyncClient,
|
|
54
|
+
) -> None:
|
|
55
|
+
offer = runtime_compatibility_offer("windy.direct.openai-compatible.v1")
|
|
56
|
+
pinned = await start(
|
|
57
|
+
client,
|
|
58
|
+
"current-pin",
|
|
59
|
+
{
|
|
60
|
+
"X-Windy-Agent-Runtime-Protocol": "1",
|
|
61
|
+
"X-Windy-Agent-Runtime-Offer-Digest": str(offer["offerDigest"]),
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert pinned.status_code == 200
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize(
|
|
69
|
+
"headers",
|
|
70
|
+
[
|
|
71
|
+
{"X-Windy-Agent-Runtime-Protocol": "1"},
|
|
72
|
+
{"X-Windy-Agent-Runtime-Offer-Digest": "sha256:" + "0" * 64},
|
|
73
|
+
{
|
|
74
|
+
"X-Windy-Agent-Runtime-Protocol": "2",
|
|
75
|
+
"X-Windy-Agent-Runtime-Offer-Digest": "sha256:" + "0" * 64,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"X-Windy-Agent-Runtime-Protocol": "1",
|
|
79
|
+
"X-Windy-Agent-Runtime-Offer-Digest": "sha256:" + "0" * 64,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
async def test_start_rejects_partial_or_incompatible_pin_before_admission(
|
|
84
|
+
client: httpx.AsyncClient,
|
|
85
|
+
headers: dict[str, str],
|
|
86
|
+
) -> None:
|
|
87
|
+
response = await start(client, "rejected-pin", headers)
|
|
88
|
+
|
|
89
|
+
assert response.status_code == 409
|
|
90
|
+
assert response.json() == {
|
|
91
|
+
"code": "RUNTIME_UNAVAILABLE",
|
|
92
|
+
"message": "Agent Runtime compatibility pin 不匹配",
|
|
93
|
+
"requestId": response.json()["requestId"],
|
|
94
|
+
"retryable": False,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def start(
|
|
99
|
+
client: httpx.AsyncClient,
|
|
100
|
+
key: str,
|
|
101
|
+
headers: dict[str, str] | None = None,
|
|
102
|
+
) -> httpx.Response:
|
|
103
|
+
return await client.post(
|
|
104
|
+
"/api/agent/runs",
|
|
105
|
+
headers={
|
|
106
|
+
"Authorization": f"Bearer {TOKEN}",
|
|
107
|
+
"Idempotency-Key": key,
|
|
108
|
+
**(headers or {}),
|
|
109
|
+
},
|
|
110
|
+
json={"operationKey": "test.echo", "input": {"text": key}},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def public_fixture() -> dict[str, object]:
|
|
115
|
+
path = (
|
|
116
|
+
Path(__file__).parents[3]
|
|
117
|
+
/ "packages/server-sdk/contracts/agent-runtime-compatibility/v1/offer.json"
|
|
118
|
+
)
|
|
119
|
+
return cast(dict[str, object], json.loads(path.read_text(encoding="utf8")))
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createAgentRuntimeCompatibilityOfferV1 } from "@southwind-ai/server-sdk";
|
|
3
|
+
import type { AgentRuntimeOperation } from "./run-contracts.js";
|
|
4
|
+
import { RemoteAgentRuntime } from "./remote-runtime.js";
|
|
5
|
+
|
|
6
|
+
const servers: Bun.Server<unknown>[] = [];
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
for (const server of servers.splice(0)) server.stop(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("RemoteAgentRuntime compatibility", () => {
|
|
13
|
+
test("V1 协商成功后固定协议版本与 Offer 摘要", async () => {
|
|
14
|
+
let startHeaders: Headers | undefined;
|
|
15
|
+
const offer = createAgentRuntimeCompatibilityOfferV1(
|
|
16
|
+
"windy.direct.openai-compatible.v1",
|
|
17
|
+
);
|
|
18
|
+
const server = Bun.serve({
|
|
19
|
+
port: 0,
|
|
20
|
+
fetch(request) {
|
|
21
|
+
if (isCompatibilityRequest(request)) return Response.json(offer);
|
|
22
|
+
startHeaders = request.headers;
|
|
23
|
+
return accepted("run_pinned");
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
servers.push(server);
|
|
27
|
+
const runtime = remote(server, "require-v1");
|
|
28
|
+
|
|
29
|
+
await runtime.start(agentOperation(), "pinned-1");
|
|
30
|
+
|
|
31
|
+
expect(startHeaders?.get("x-windy-agent-runtime-protocol")).toBe("1");
|
|
32
|
+
expect(startHeaders?.get("x-windy-agent-runtime-offer-digest")).toBe(
|
|
33
|
+
offer.offerDigest,
|
|
34
|
+
);
|
|
35
|
+
expect(startHeaders?.get("authorization")).toBe("Bearer test-token");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("prefer-v1 只对旧 Runtime 的 404 回退为 legacy", async () => {
|
|
39
|
+
let starts = 0;
|
|
40
|
+
const server = Bun.serve({
|
|
41
|
+
port: 0,
|
|
42
|
+
fetch(request) {
|
|
43
|
+
if (isCompatibilityRequest(request)) {
|
|
44
|
+
return new Response(null, { status: 404 });
|
|
45
|
+
}
|
|
46
|
+
starts += 1;
|
|
47
|
+
return accepted("run_legacy");
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
servers.push(server);
|
|
51
|
+
|
|
52
|
+
await expect(
|
|
53
|
+
remote(server, "prefer-v1").start(agentOperation(), "legacy-1"),
|
|
54
|
+
).resolves.toEqual({
|
|
55
|
+
runId: "run_legacy",
|
|
56
|
+
status: "accepted",
|
|
57
|
+
eventsUrl: "/api/agent/runs/run_legacy/events",
|
|
58
|
+
});
|
|
59
|
+
expect(starts).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("require-v1 拒绝旧 Runtime,畸形 Offer 也不会降级", async () => {
|
|
63
|
+
for (const response of [
|
|
64
|
+
new Response(null, { status: 404 }),
|
|
65
|
+
Response.json({ schemaVersion: 1 }),
|
|
66
|
+
]) {
|
|
67
|
+
let starts = 0;
|
|
68
|
+
const server = Bun.serve({
|
|
69
|
+
port: 0,
|
|
70
|
+
fetch(request) {
|
|
71
|
+
if (isCompatibilityRequest(request)) return response.clone();
|
|
72
|
+
starts += 1;
|
|
73
|
+
return Response.json({});
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
servers.push(server);
|
|
77
|
+
|
|
78
|
+
await expect(
|
|
79
|
+
remote(server, "require-v1").start(agentOperation(), "required-1"),
|
|
80
|
+
).rejects.toMatchObject({
|
|
81
|
+
code: "RUNTIME_UNAVAILABLE",
|
|
82
|
+
retryable: false,
|
|
83
|
+
});
|
|
84
|
+
expect(starts).toBe(0);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function remote(server: Bun.Server<unknown>, mode: "prefer-v1" | "require-v1") {
|
|
90
|
+
return new RemoteAgentRuntime({
|
|
91
|
+
baseUrl: server.url.toString(),
|
|
92
|
+
internalToken: "test-token",
|
|
93
|
+
toolHostUrl: "http://server.internal",
|
|
94
|
+
compatibilityMode: mode,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isCompatibilityRequest(request: Request): boolean {
|
|
99
|
+
return new URL(request.url).pathname === "/api/agent/runtime/compatibility";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function accepted(runId: string): Response {
|
|
103
|
+
return Response.json({
|
|
104
|
+
runId,
|
|
105
|
+
status: "accepted",
|
|
106
|
+
eventsUrl: `/api/agent/runs/${runId}/events`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function agentOperation(): AgentRuntimeOperation {
|
|
111
|
+
return {
|
|
112
|
+
kind: "agent",
|
|
113
|
+
operationKey: "business.answer",
|
|
114
|
+
input: { question: "你好" },
|
|
115
|
+
inputModalities: ["text"],
|
|
116
|
+
requiredCapabilities: ["text-generation"],
|
|
117
|
+
artifacts: [],
|
|
118
|
+
instructions: "回答业务问题",
|
|
119
|
+
userPrompt: "你好",
|
|
120
|
+
outputSchemaKey: "business.output.v1",
|
|
121
|
+
allowedTools: [],
|
|
122
|
+
limits: {
|
|
123
|
+
maxDurationSeconds: 60,
|
|
124
|
+
maxInputBytes: 4_096,
|
|
125
|
+
maxOutputBytes: 4_096,
|
|
126
|
+
},
|
|
127
|
+
providerEgress: {
|
|
128
|
+
mode: "configured-endpoint",
|
|
129
|
+
deploymentKey: "ai.openai-compatible",
|
|
130
|
+
},
|
|
131
|
+
providerDataPolicy: { inputFields: [], envelopeFields: [] },
|
|
132
|
+
toolNetworkPolicy: { mode: "forbidden" },
|
|
133
|
+
executionGrant: "grant_1",
|
|
134
|
+
release: {
|
|
135
|
+
releaseId: "release_test",
|
|
136
|
+
operationVersion: "1.0.0",
|
|
137
|
+
workflowVersion: "workflow-1",
|
|
138
|
+
promptVersion: "prompt-1",
|
|
139
|
+
routeVersion: "route-1",
|
|
140
|
+
outputSchemaVersion: "output-schema-1",
|
|
141
|
+
toolSchemaVersion: "schema-1",
|
|
142
|
+
manifestDigest: "0".repeat(64),
|
|
143
|
+
},
|
|
144
|
+
traceId: "trace_1",
|
|
145
|
+
reporting: { actorId: "user_1", actorType: "user" },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
AgentRuntimeError,
|
|
3
|
+
type AgentRuntimeOperation,
|
|
4
|
+
type AgentRuntimePort,
|
|
5
|
+
type AgentRuntimeStartResponse,
|
|
5
6
|
} from "./run-contracts.js";
|
|
7
|
+
import {
|
|
8
|
+
AGENT_RUNTIME_COMPATIBILITY_PATH_V1,
|
|
9
|
+
AGENT_RUNTIME_OFFER_DIGEST_HEADER_V1,
|
|
10
|
+
AGENT_RUNTIME_PROTOCOL_HEADER_V1,
|
|
11
|
+
localAgentRuntimeCompatibilityRequirementsV1,
|
|
12
|
+
negotiateAgentRuntimeCompatibilityV1,
|
|
13
|
+
} from "@southwind-ai/server-sdk";
|
|
6
14
|
import { assertVisionReadinessPin } from "./vision-readiness-pin.js";
|
|
7
15
|
import {
|
|
8
16
|
parseRemoteStart,
|
|
@@ -15,6 +23,8 @@ export interface RemoteAgentRuntimeOptions {
|
|
|
15
23
|
internalToken: string;
|
|
16
24
|
toolHostUrl: string;
|
|
17
25
|
timeoutMs?: number;
|
|
26
|
+
/** Legacy is the default for direct callers; platform composition opts into rolling V1. */
|
|
27
|
+
compatibilityMode?: "prefer-v1" | "require-v1";
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
export class RemoteAgentRuntime implements AgentRuntimePort {
|
|
@@ -33,6 +43,7 @@ export class RemoteAgentRuntime implements AgentRuntimePort {
|
|
|
33
43
|
request: AgentRuntimeOperation,
|
|
34
44
|
idempotencyKey: string,
|
|
35
45
|
): Promise<AgentRuntimeStartResponse> {
|
|
46
|
+
const compatibilityHeaders = await this.compatibilityHeaders();
|
|
36
47
|
if (request.readinessPin) {
|
|
37
48
|
await assertVisionReadinessPin(request.readinessPin, () => this.health());
|
|
38
49
|
}
|
|
@@ -103,6 +114,7 @@ export class RemoteAgentRuntime implements AgentRuntimePort {
|
|
|
103
114
|
headers: {
|
|
104
115
|
"content-type": "application/json",
|
|
105
116
|
"idempotency-key": idempotencyKey,
|
|
117
|
+
...compatibilityHeaders,
|
|
106
118
|
},
|
|
107
119
|
body: JSON.stringify(remoteRequest),
|
|
108
120
|
});
|
|
@@ -160,6 +172,38 @@ export class RemoteAgentRuntime implements AgentRuntimePort {
|
|
|
160
172
|
return { deploymentKey, model };
|
|
161
173
|
}
|
|
162
174
|
|
|
175
|
+
private async compatibilityHeaders(): Promise<Record<string, string>> {
|
|
176
|
+
const mode = this.options.compatibilityMode;
|
|
177
|
+
if (!mode) return {};
|
|
178
|
+
let response: Response;
|
|
179
|
+
try {
|
|
180
|
+
response = await this.request(AGENT_RUNTIME_COMPATIBILITY_PATH_V1);
|
|
181
|
+
} catch {
|
|
182
|
+
throw new AgentRuntimeError(
|
|
183
|
+
"RUNTIME_UNAVAILABLE",
|
|
184
|
+
"Agent Runtime compatibility 探测失败",
|
|
185
|
+
true,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (response.status === 404 && mode === "prefer-v1") return {};
|
|
189
|
+
if (!response.ok) throw await remoteRuntimeError(response);
|
|
190
|
+
let value: unknown;
|
|
191
|
+
try {
|
|
192
|
+
value = await readBoundedCompatibilityOffer(response);
|
|
193
|
+
} catch {
|
|
194
|
+
throw incompatibleRuntime("CONTRACT_INVALID");
|
|
195
|
+
}
|
|
196
|
+
const negotiation = negotiateAgentRuntimeCompatibilityV1(
|
|
197
|
+
value,
|
|
198
|
+
localAgentRuntimeCompatibilityRequirementsV1(),
|
|
199
|
+
);
|
|
200
|
+
if (!negotiation.ok) throw incompatibleRuntime(negotiation.code);
|
|
201
|
+
return {
|
|
202
|
+
[AGENT_RUNTIME_PROTOCOL_HEADER_V1]: String(negotiation.protocolVersion),
|
|
203
|
+
[AGENT_RUNTIME_OFFER_DIGEST_HEADER_V1]: negotiation.offer.offerDigest,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
163
207
|
private async request(
|
|
164
208
|
path: string,
|
|
165
209
|
init: RequestInit = {},
|
|
@@ -187,6 +231,24 @@ export class RemoteAgentRuntime implements AgentRuntimePort {
|
|
|
187
231
|
}
|
|
188
232
|
}
|
|
189
233
|
|
|
234
|
+
function incompatibleRuntime(code: string): AgentRuntimeError {
|
|
235
|
+
return new AgentRuntimeError(
|
|
236
|
+
"RUNTIME_UNAVAILABLE",
|
|
237
|
+
`Agent Runtime compatibility 不匹配(${code})`,
|
|
238
|
+
false,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function readBoundedCompatibilityOffer(
|
|
243
|
+
response: Response,
|
|
244
|
+
): Promise<unknown> {
|
|
245
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
246
|
+
if (bytes.byteLength > 64 * 1_024) {
|
|
247
|
+
throw new Error("Agent Runtime compatibility Offer 超过上限");
|
|
248
|
+
}
|
|
249
|
+
return JSON.parse(new TextDecoder().decode(bytes)) as unknown;
|
|
250
|
+
}
|
|
251
|
+
|
|
190
252
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
191
253
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
192
254
|
}
|
|
@@ -104,6 +104,7 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
|
|
|
104
104
|
baseUrl: agentServerUrl,
|
|
105
105
|
internalToken,
|
|
106
106
|
toolHostUrl: process.env.AGENT_TOOL_HOST_URL || "http://server:9747",
|
|
107
|
+
compatibilityMode: "prefer-v1",
|
|
107
108
|
});
|
|
108
109
|
const clientAgentSessionRuntime = createClientAgentSessionRuntime({
|
|
109
110
|
app: input.app,
|
|
@@ -33,6 +33,7 @@ describe("Modal 布局", () => {
|
|
|
33
33
|
"min-h-[min(12rem,calc(100dvh-2rem))]",
|
|
34
34
|
);
|
|
35
35
|
expect(content?.className).toContain("overflow-hidden");
|
|
36
|
+
expect(content?.className).toContain("sm:max-w-2xl");
|
|
36
37
|
expect(body?.className).toContain("overflow-y-auto");
|
|
37
38
|
expect(footer?.className).toContain("shrink-0");
|
|
38
39
|
|
|
@@ -44,7 +44,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
|
44
44
|
v-bind="{ ...$attrs, ...forwarded }"
|
|
45
45
|
:class="
|
|
46
46
|
cn(
|
|
47
|
-
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 fixed top-1/2 left-1/2 z-50 flex min-h-[min(12rem,calc(100dvh-2rem))] max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 overflow-hidden rounded-xl p-4 text-sm ring-1 duration-100 outline-none sm:max-w-
|
|
47
|
+
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 fixed top-1/2 left-1/2 z-50 flex min-h-[min(12rem,calc(100dvh-2rem))] max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 overflow-hidden rounded-xl p-4 text-sm ring-1 duration-100 outline-none sm:max-w-xl',
|
|
48
48
|
props.class,
|
|
49
49
|
)
|
|
50
50
|
"
|
|
@@ -35,7 +35,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
|
35
35
|
<DialogContent
|
|
36
36
|
:class="
|
|
37
37
|
cn(
|
|
38
|
-
'relative z-50 grid w-full max-w-
|
|
38
|
+
'relative z-50 my-8 grid w-full max-w-2xl gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
|
39
39
|
props.class,
|
|
40
40
|
)
|
|
41
41
|
"
|